Compare commits

..

3 Commits

Author SHA1 Message Date
f65c24ffcd fix: latest lesson 2026-03-31 20:59:53 +02:00
426b9f89d9 fix: add 9 2026-03-31 16:34:28 +02:00
b9ffee586f fix: add les 9 2026-03-31 16:18:33 +02:00
28 changed files with 3979 additions and 3758 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

207
Les08-Docenttekst.md Normal file
View File

@@ -0,0 +1,207 @@
# 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.

346
Les08-Lesopdracht.pdf Normal file
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 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

186
Les08-Slide-Overzicht.md Normal file
View File

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

BIN
Les08-Slides.pptx Normal file

Binary file not shown.

View File

@@ -0,0 +1,575 @@
# 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

@@ -0,0 +1,238 @@
%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

@@ -0,0 +1,551 @@
# 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

@@ -0,0 +1,169 @@
# 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

Binary file not shown.

View File

@@ -1,893 +0,0 @@
# Les 8 — Docenttekst
## Supabase × Next.js + Auth
---
## Lesoverzicht
| Gegeven | Details |
|---------|---------|
| **Les** | 8 van 18 |
| **Onderwerp** | Supabase koppelen + Auth introductie |
| **Duur** | 3 uur (09:00 12:00) |
| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen, RLS ingesteld |
| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account |
---
## Leerdoelen
Na deze les kunnen studenten:
1. De Supabase JavaScript client gebruiken in een Next.js project
2. Data ophalen via Supabase queries (select met relaties, eq, single)
3. Het Server Component + Client Component patroon toepassen
4. Uitleggen wat authenticatie vs autorisatie is
5. Supabase Auth functies gebruiken (signUp, signIn, signOut, getUser)
6. Een login/registratie flow bouwen in Next.js
---
## Lesvoorbereiding (voor docent)
Zorg dat je volgende zaken hebt voorbereiding:
- Een werkend Supabase project met `polls` en `options` tabellen (uit Les 7)
- RLS ingeschakeld op beide tabellen met policies voor SELECT (anon) en UPDATE (anon op options)
- De Next.js QuickPoll app uit Les 7 werkend op je machine
- De slides gereed voor uitleg authenticatie vs autorisatie
- Test je eigen Supabase credentials vooraf
---
## 09:0009:10 | Welkom & Terugblik (10 min)
**Doel:** Studenten krijgen duidelijk wat we vandaag doen en waar we van vorige week waren.
### Wat we hebben gedaan in Les 7:
- ✅ Stemmen werkend gemaakt (votePoll functie, state update in poll detail page)
- ✅ Supabase introductie: account aangemaakt, project gemaakt
- ✅ Database: polls + options tabellen aangemaakt
- ✅ Foreign keys + CASCADE ingesteld
- ✅ RLS policies ingesteld (SELECT voor anon, UPDATE voor anon op options)
- ✅ Testdata ingevoerd via Table Editor
### Wat we NIET hebben afgemaakt in Les 7:
- ❌ Supabase is NIET aan het Next.js project gekoppeld
- ❌ Data wordt nog niet uit Supabase opgehaald
- ❌ Geen authenticatie
### Vandaag gaan we:
1. **DEEL 1 (65 min):** Supabase client installeren en opzetten → data uit database halen in Next.js
2. **DEEL 2a (30 min):** Uitleg over authenticatie, autorisatie en Supabase Auth features
3. **DEEL 2b (30 min):** Studenten bouwen auth zelf in hun project (signup, login, logout)
**Motivatie:** "Tot nu toe zijn je polls hardcoded in geheugen. Straks halen we echte data uit Supabase en kunnen people inloggen. Dat is een echt web app!"
---
## 09:1010:15 | DEEL 1: Supabase Koppelen — Live Coding (65 min)
Dit deel volgt een stap-voor-stap aanpak met live coding. Alle studenten coderen mee.
### 09:1009:15 | Installatie (5 min)
Open terminal in het QuickPoll project en run:
```bash
npm install @supabase/supabase-js
```
**Teacher Tip:** Controleer dat de installatie slaagt. Als students `npm ERR!` zien, laat ze eerst `npm clean-install` doen en daarna opnieuw proberen.
### 09:1509:25 | Environment Variables (10 min)
Zorg dat alle studenten hun Supabase credentials veilig opslaan.
1. Open in Supabase Dashboard: **Settings****API**
2. Kopieer:
- `Project URL` (eindigt op `.supabase.co`)
- `anon` public key
3. Maak/open `.env.local` in je Next.js project root:
```env
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
```
**Belangrijk:**
- `.env.local` staat al in `.gitignore` (check even)
- Keys die beginnen met `NEXT_PUBLIC_` zijn zichtbaar in browser (maar anon keys zijn daarvoor bedoeld)
- **ALTIJD de dev server herstarten na wijzigen van `.env.local`** (Ctrl+C, dan `npm run dev`)
**Teacher Tip:** Dit is een veelvoorkomende fout. Zeg hardop: "Als jullie een leeg array zien in plaats van polls, check EERST of je dev server herstarten hebt!"
### 09:2509:35 | Supabase Client aanmaken (10 min)
Maak `lib/supabase.ts`:
```typescript
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseKey)
```
**Wat gebeurt hier:**
- We importeren `createClient` uit `@supabase/supabase-js`
- We halen URL en key uit environment variables
- We geven deze aan `createClient`
- We exporteren de client zodat we het overal kunnen gebruiken
**Teacher Tip:** TypeScript geeft mogelijk een warning over "null assertion (!)" — dat is OK. Dit zeggen we tegen TypeScript: "Deze values bestaan echt, vertrouw me."
### 09:3509:45 | Database Types (10 min)
Maak `lib/types.ts` handmatig:
```typescript
export interface Poll {
id: string
created_at: string
question: string
}
export interface Option {
id: string
poll_id: string
text: string
votes: number
}
```
**Waarom:** Dit helpt TypeScript begrijpen welke data we uit Supabase krijgen.
**Teacher Tip:** In een echt project zou je `npx supabase gen types typescript` gebruiken, maar dat kost extra setup. Voor deze les is handmatig OK.
### 09:4510:00 | Async Data functies (15 min)
Update `lib/data.ts` — alle functies worden nu async en halen data uit Supabase:
```typescript
import { supabase } from './supabase'
import { Poll, Option } from './types'
export async function getPolls(): Promise<Poll[]> {
const { data, error } = await supabase
.from('polls')
.select('*')
.order('created_at', { ascending: false })
if (error) {
console.error('Error fetching polls:', error)
return []
}
return data || []
}
export async function getOptions(pollId: string): Promise<Option[]> {
const { data, error } = await supabase
.from('options')
.select('*')
.eq('poll_id', pollId)
.order('votes', { ascending: false })
if (error) {
console.error('Error fetching options:', error)
return []
}
return data || []
}
```
**Wat betekent dit:**
- `.from('polls')` — welke tabel
- `.select('*')` — alle kolommen
- `.eq('poll_id', pollId)` — filter op poll_id
- `.order()` — sorteer op
- `await` — wacht op het resultaat van de database call
- Error handling — log en return empty array
**Teacher Tip:** Veel students maken hier fouten met async/await:
```typescript
// ❌ FOUT: promise niet awaited!
const data = supabase.from('polls').select('*')
// ✅ GOED:
const data = await supabase.from('polls').select('*')
```
### 10:0010:10 | Homepage als Server Component (10 min)
Update `app/page.tsx` — dit wordt een Server Component:
```typescript
import { getPolls } from '@/lib/data'
import PollItem from '@/components/PollItem'
export default async function Home() {
const polls = await getPolls()
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">QuickPoll</h1>
<div className="space-y-4">
{polls.map((poll) => (
<PollItem key={poll.id} poll={poll} />
))}
</div>
{polls.length === 0 && (
<p className="text-gray-500">Geen polls beschikbaar.</p>
)}
</div>
)
}
```
**Belangrijk:** Page.tsx is nu een **Server Component** — geen `'use client'` directive! We kunnen hier `async/await` rechtstreeks gebruiken.
**Teacher Tip:** Students vragen: "Maar hoe krijgen we de options?" — Goed punt! Die halen we in PollItem.
### 10:1010:15 | PollItem Component (5 min)
Update `components/PollItem.tsx` — ook een Server Component:
```typescript
import { getOptions } from '@/lib/data'
import VoteForm from './VoteForm'
import { Poll } from '@/lib/types'
export default async function PollItem({ poll }: { poll: Poll }) {
const options = await getOptions(poll.id)
return (
<div className="border rounded-lg p-4">
<h2 className="text-lg font-semibold mb-3">{poll.question}</h2>
<div className="space-y-2">
{options.map((option) => (
<VoteForm
key={option.id}
option={option}
pollId={poll.id}
/>
))}
</div>
</div>
)
}
```
**Waarom twee Server Components?**
- `page.tsx` ziet alleen alle polls (geen details)
- `PollItem` wordt per poll gerenderd en haalt zelf de options op (parallel!)
- Dit patroon is efficient en schaalbaar
**Teacher Tip:** Dit is het "Suspended Components" patroon van React 18 — Server Components voeren dit automatisch in parallel uit.
---
## 10:1510:30 | PAUZE (15 min)
Goed moment om even weg te lopen. Tussendoor kun jij:
- Rondlopen en kijken wie nog errors heeft
- Checken of iedereen env vars juist ingesteld heeft
- Dev servers herstarten voor wie vergeten zijn
- Voorbereiding treffen voor DEEL 2
---
## 10:3011:00 | DEEL 2a: Uitleg Auth (30 min)
Dit is uitleg — geen live coding nog. Zorg dat alle laptops dicht zijn, focus op slides en beamer.
### Authenticatie vs Autorisatie
**Authenticatie (Authentication):**
- "Wie ben je?" — identity verification
- Voorbeeld: Je logt in met email + password
- Supabase Auth zorgt hiervoor
**Autorisatie (Authorization):**
- "Wat mag je?" — permissions
- Voorbeeld: Je mag alleen je eigen polls aanpassen
- RLS (Row Level Security) in Supabase zorgt hiervoor
**Voorbeeld:**
- Auth: "Je email en password kloppen, je bent Alice."
- RLS: "Alice mag haar eigen polls zien en updaten, maar niet die van Bob."
### Supabase Auth Features
Demo op beamer:
1. Open Supabase Dashboard → **Authentication****Providers**
2. Toon dat **Email/Password** is ingeschakeld
3. Toon de instelling **"Confirm email"** (nu UIT voor dev)
4. Ga naar **Users** tab — hier zie je ingelogde users
**Supabase Auth ondersteunt:**
- Email/Password (wat we vandaag gebruiken)
- OAuth (Google, GitHub, etc.) — volgende week
- Magic Links (passwordless login)
- Session management (Supabase beheert cookies automatisch)
### @supabase/ssr vs @supabase/supabase-js
**@supabase/supabase-js:**
- Browser-side client
- Voor onClick handlers, forms, interactie
**@supabase/ssr:**
- Server-side client (SSR = Server-Side Rendering)
- Voor middleware, cookies, server actions
- Handelt sessions automatisch af
**Waarom twee?**
- Browser kan niet veilig geheimen beheren
- Server kan veilig cookies zetten
- Supabase SSR packages zorgen dat beide veilig werken
### Supabase Auth Functies
**signUp(email, password)** — nieuwe account aanmaken
```typescript
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password'
})
```
**signInWithPassword(email, password)** — inloggen
```typescript
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
```
**signOut()** — uitloggen
```typescript
await supabase.auth.signOut()
```
**getUser()** — huidge user ophalen
```typescript
const { data: { user } } = await supabase.auth.getUser()
// user is null als niemand ingelogd, anders is het een User object
```
### Server vs Browser Client
**Browser Client (createBrowserClient):**
- Voor 'use client' components
- Kan useState gebruiken
- Kan useRouter gebruiken
- Kan user events luisteren
**Server Client (createServerClient):**
- Voor server components en middleware
- Leest/schrijft cookies
- Kan getUser() veilig aanroepen
- Geen access tot browser APIs
### Middleware & Session Refresh
**Wat doet middleware?**
- Draait op elke request naar je app
- Refreshed de Supabase session
- Zorgt dat user state altijd up-to-date is
**Voorbeeld flow:**
1. User logt in op `/login` page
2. Cookie wordt gezet
3. Middleware ziet op volgende request: "Er is een session cookie!"
4. Middleware refreshed de session
5. App ziet dat user ingelogd is
### Handige links
Toon op slides:
- [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs)
- [Next.js Server Components docs](https://nextjs.org/docs/getting-started/react-essentials)
---
## 11:0011:30 | DEEL 2b: Zelf Doen — Auth Implementeren (30 min)
Nu gaan studenten zelf auth bouwen in hun project. Dit is niet meer live coding — docent loopt rond en helpt.
**Instructie voor studenten:**
Volg deze stappen. Docent loopt rond als je vragen hebt.
#### Stap 1: SSR Package Installeren (2 min)
```bash
npm install @supabase/ssr
```
#### Stap 2: Server Client (3 min)
Maak `lib/supabase-server.ts`:
```typescript
import { cookies } from 'next/headers'
import { createServerClient } from '@supabase/ssr'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Handle error
}
},
},
}
)
}
```
**Wat is dit?** Dit is een helper zodat Supabase cookies kan beheren in Next.js. Copy-paste voor nu.
#### Stap 3: Browser Client (1 min)
Maak `lib/supabase-browser.ts`:
```typescript
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
```
**Wat is dit?** Dit gebruiken we in 'use client' components.
#### Stap 4: Middleware (5 min)
Maak `middleware.ts` in project root:
```typescript
import { type NextRequest, NextResponse } from 'next/server'
import { createServerClient } from '@supabase/ssr'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
supabaseResponse.cookies.set(name, value, options)
})
},
},
}
)
// Refresh user session
await supabase.auth.getUser()
return supabaseResponse
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.svg|.*\\.png|.*\\.jpg|.*\\.jpeg).*)',
],
}
```
**Wat is dit?** Dit draait op elke request en refreshed de session. Copy-paste, don't worry.
#### Stap 5: Signup Page (5 min)
Maak `app/auth/signup/page.tsx`:
```typescript
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase-browser'
export default function SignUpPage() {
const router = useRouter()
const supabase = createClient()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) {
setError(error.message)
setLoading(false)
} else {
router.push('/auth/login')
}
}
return (
<div className="flex items-center justify-center min-h-screen">
<form onSubmit={handleSignUp} className="w-full max-w-md p-6 border rounded-lg">
<h1 className="text-2xl font-bold mb-6">Sign Up</h1>
{error && <div className="text-red-600 mb-4">{error}</div>}
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border rounded mb-4"
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border rounded mb-6"
required
/>
<button
type="submit"
disabled={loading}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Signing up...' : 'Sign Up'}
</button>
</form>
</div>
)
}
```
**Belangrijk:** `'use client'` directive bovenaan — dit is een interactive component!
#### Stap 6: Login Page (5 min)
Maak `app/auth/login/page.tsx`:
```typescript
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase-browser'
import Link from 'next/link'
export default function LoginPage() {
const router = useRouter()
const supabase = createClient()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
setError(error.message)
setLoading(false)
} else {
router.push('/')
}
}
return (
<div className="flex items-center justify-center min-h-screen">
<form onSubmit={handleLogin} className="w-full max-w-md p-6 border rounded-lg">
<h1 className="text-2xl font-bold mb-6">Login</h1>
{error && <div className="text-red-600 mb-4">{error}</div>}
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 border rounded mb-4"
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 border rounded mb-6"
required
/>
<button
type="submit"
disabled={loading}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Logging in...' : 'Login'}
</button>
<p className="mt-4 text-center text-sm">
Nog geen account? <Link href="/auth/signup" className="text-blue-600 hover:underline">Sign up</Link>
</p>
</form>
</div>
)
}
```
#### Stap 7: Logout Button (3 min)
Maak `components/LogoutButton.tsx`:
```typescript
'use client'
import { useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase-browser'
export default function LogoutButton() {
const router = useRouter()
const supabase = createClient()
const handleLogout = async () => {
await supabase.auth.signOut()
router.refresh()
}
return (
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Logout
</button>
)
}
```
**Belangrijk:** `router.refresh()` na logout zorgt dat page de nieuwe state ziet!
#### Stap 8: Navbar met Auth State (3 min)
Update `components/Navbar.tsx`:
```typescript
import { createClient } from '@/lib/supabase-server'
import Link from 'next/link'
import LogoutButton from './LogoutButton'
export default async function Navbar() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
return (
<nav className="bg-gray-800 text-white p-4">
<div className="container mx-auto flex justify-between items-center">
<Link href="/" className="text-2xl font-bold">
QuickPoll
</Link>
<div className="flex gap-4 items-center">
{user ? (
<>
<span className="text-sm">{user.email}</span>
<LogoutButton />
</>
) : (
<>
<Link href="/auth/login" className="px-4 py-2 bg-blue-600 rounded hover:bg-blue-700">
Login
</Link>
<Link href="/auth/signup" className="px-4 py-2 bg-green-600 rounded hover:bg-green-700">
Sign Up
</Link>
</>
)}
</div>
</div>
</nav>
)
}
```
**Logica:**
- Als `user` bestaat (ingelogd): toon email + Logout button
- Anders: toon Login + Sign Up buttons
#### Stap 9: Layout updaten (2 min)
Update `app/layout.tsx`:
```typescript
import type { Metadata } from 'next'
import Navbar from '@/components/Navbar'
import './globals.css'
export const metadata: Metadata = {
title: 'QuickPoll',
description: 'Vote on polls',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Navbar />
{children}
</body>
</html>
)
}
```
Voeg gewoon `<Navbar />` toe.
**Teacher Tip: Studenten vastlopen?**
- Na 5-10 minuten vastzitten: toon de referentie code op beamer
- Zeg: "Dit is complex, copy-paste is OK. Focus op begrijpen, niet op typen."
- Help met debuggen (console.log, errors lezen)
---
## 11:3011:45 | Vragen & Reflectie (15 min)
Hier zijn veelvoorkomende vragen:
### V: "Wat is het verschil tussen `createClient()` in server.ts en browser.ts?"
**A:**
- `server.ts`: kan cookies veilig beheren (server-side)
- `browser.ts`: kan UI events afhandelen (onClick, forms)
- Supabase kiest automatisch het juiste moment om te gebruiken
### V: "Waarom twee environment variables bovenaan?"
**A:**
- `NEXT_PUBLIC_SUPABASE_URL`: URL is public, iedereen ziet het
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`: anon key is public (maar kan geen private data lezen)
- Private keys (service role) zetten we NIET in .env.local, die gaan in server.ts als geheim
### V: "Mijn login werkt niet, ik krijg error"
**A:** Check:
1. Klopt je email/password echt?
2. Is je account in Supabase Dashboard → Authentication → Users?
3. Is Email provider ingeschakeld?
4. Zit "Confirm email" uit? (check dashboard)
### V: "Logout werkt niet, user staat nog ingelogd"
**A:** Vergeten `router.refresh()` na `signOut()`?
### V: "Middleware error: 'createServerClient is not defined'"
**A:** Check je import: moet `import { createServerClient } from '@supabase/ssr'` zijn
### V: "Kan ik als anonieme user stemmen?"
**A:** Ja! RLS policy staat op `FOR SELECT, UPDATE TO authenticated` — maar je Navbar toont Login/Signup want je bent nog niet ingelogd. Dat is OK. Volgende les doen we RLS policies correct.
---
## 11:4512:00 | Huiswerk & Afsluiting (15 min)
### Huiswerk (voor Les 9):
**Verplicht:**
1. **/create pagina bouwen** — studenten voegen nieuwe polls toe via een form
- Maak `app/create/page.tsx` (Server Component met form als Client Component)
- Form met: vraag + array van 2-3 opties
- `supabase.from('polls').insert()` en `supabase.from('options').insert()`
- Zorg dat je eigen `user_id` meestuurt
2. **RLS INSERT policy** — alleen authenticated users mogen polls toevoegen
- Supabase Dashboard → Authentication → Policies
- Voeg policy toe: `INSERT` voor authenticated users
- `user_id = auth.uid()`
3. **Optional extras (challenge):**
- Toon poll creator in PollItem
- Google OAuth inschakelen (zie Supabase docs)
- Edit/Delete buttons (alleen voor je eigen polls)
### Afsluitingsboodschap:
"Gefeliciteerd! Vandaag hebben jullie:
- Supabase gekoppeld aan Next.js
- Real data uit een database geladen
- Login/logout gebouwd
- Server & browser clients begrepen
Volgende week voegen we RLS policies toe zodat iedereen alleen zijn eigen polls kan aanpassen. Dat is waar authenticatie écht nuttig wordt!"
---
## Veelvoorkomende Problemen
| Probleem | Oorzaak | Oplossing |
|----------|---------|-----------|
| `Error: Cannot find module '@supabase/supabase-js'` | Package niet geïnstalleerd | `npm install @supabase/supabase-js` en dev server herstarten |
| Supabase returns leeg array | .env.local niet juist of dev server niet herstarten | Check .env.local, restart dev server (Ctrl+C + `npm run dev`) |
| TypeScript complains over `null assertion (!)` | Normale TS warning | Dit is OK, we vertellen TS dat env vars bestaan |
| `'use client' vergeten in signup/login page` | Component is interactief maar geen directive | Voeg `'use client'` bovenaan toe |
| Login page blank/geen content | Conflict met server components | Zorg ALL pages onder /auth zijn `'use client'` |
| Logout werkt niet, user nog ingelogd | `router.refresh()` niet aangeroepen | Voeg `await router.refresh()` toe na `signOut()` |
| Middleware error: "wrong params" | Onjuiste URL of key in middleware | Copy-paste van .env.local, check Format |
| "Invalid token" bij Supabase calls | Token verlopen of anon key fout | Restart dev server, check API credentials |
| User niet in Authentication → Users | Signup failed, geen account aangemaakt | Check browser console op errors, probeer opnieuw met ander email |
| `router.refresh()` werkt niet in component | Router niet geïmporteerd | `import { useRouter } from 'next/navigation'` (niet 'next/router'!) |
| Cors/network error | Supabase URL fout | Check dat URL eindigt op `.supabase.co` en https:// bevat |
| Password te kort / validation error | Supabase vereist min 6 chars | Zeg studenten: "Test met password123" |
---
## Didactische Tips
- **Pair Programming:** Zet snelle studenten samen met tragere — kennis spreidt zich uit
- **Show & Tell:** Toon je eigen werkend QuickPoll op beamer — studenten zien het doel
- **Error-driven Learning:** Zeg niet meteen het antwoord, vraag: "Wat zegt de error?"
- **Debug together:** Als iemand vastlopen, use browser console.log + devtools
- **Save time** — als >3 students dezelfde error hebben, stop even en toon op beamer
- **Celebrate wins** — als iemand eerste Signup working heeft, geef thumbs up!
---
## Referentiematerialen voor Studenten
- [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs)
- [Next.js Server Components](https://nextjs.org/docs/getting-started/react-essentials)
- [Environment Variables in Next.js](https://nextjs.org/docs/basic-features/environment-variables)
- Alle code snippets uit deze docenttekst
---
**Einde docenttekst Les 8**

View File

@@ -1,219 +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 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 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 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
>>
endobj
15 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260331152247+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331152247+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
16 0 obj
<<
/Count 8 /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 ] /Type /Pages
>>
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 699
>>
stream
Gatm8b>R(K']&X:mLn5>=AMTGd@h(3)/+n_DR+6q2;%F,`rSB==*Epi),,f/E?%DgYKk$H1'JaQgAK5PJBoC/J#,CJKR\O[+s<DkQdC*KkdFDEU'rjQ7nKF(`F[ou`Ob,9rP:jop00+3jIe['))>bTVeA&XA#*?Aiu<U[i;ucWDsjddHa0sDq0c,90OlYaM^t/s>GZ>&T0eo1p]8\6f:hWsLc-T::dU/"pNbYHlBu+4O?g>L\R@`3GFo\qW7DC[Tq*.R\d:?YJa2#all,Lknm6<@ZIGE&a@@OL7Jd:R9RUXiKu<FWZJPt#Ca[j`&nn!3<q,LRN;c4\$T!ghrRsHe,[&#T\Cf=*]oW+LYP;M877*A]*uMVmiQ]1nN,YY?jd/j#.UNp%BiCLrl3LDE]T2/P*F#HMqcMLIh.:7A@bccI#.#Z<Tb1c?UDO+B872C(K\Qdi;<_p?lbKS'9q>FRS5(DS]Y;A0P8T(Me=[GR4(Je];%?P9iSUJ6"qC"^AlB^SneRYr@`.QN2kPJV"0TI[G'YtMf][!dnqF3;CfMP2`YQ<R>_@jDK*?6qVDc>fg1lu"GIoU>koF6QlZsZI(oCL3hmQ_AHi_\j4CA%<dM+k;50Krsm(=An]<h)2n&-I)3)acUrqbah#u=lfC]T.s350:;oS&d2E8))rPrgDPX=GtZ/E"_eVTf;$5KKIW,Q~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 882
>>
stream
GauJ!>u03/'F*Lmrebc"2D)km=0nsD!.iWC[V!.=\8ftbC[/jSG09USEtYbh<XFC*2c=_EV!"^ujS+m(BFiP+QPI;U!Jb:j#_h7B80#QEbi,prgeGlMPpm0jSE9s[,R[/7k4V"9EBmHk`<c[p/<"TV5ot/%;!Wu::@^oCU9Z'nd(VpB5)8Ak]oh\Sb@]"BGQ>]7'Q:RgTC&neF;+m%NRG!%rX6g6f*-AJ"6'eUP-/WA+J`KM,d]dir,YnSR%>E-+?c`3Rf8$j>Z7*-U2APGBnWU))AH9U@l7NN%hma`kC6!jB22.[^kBGWrmS`g#"V"NTM*,+2B_cm1%bOZeqOK^$>TlmQMRE+JUUek\<;!bZ$/l][M5Ze3*Ng"4%Xh*U\u]\S"sLt-Z9^roYuNmG\HHL[`s[I-R=Or,*N%AE@`d^2Q1`.;>bV#@SLn.e!^ctF'2qhf%<.]L2$$#c%o/02Ft$I&/PO$-tUrMg30NL1^SS&B7oiG1jC@tbG6uh.F!@G.BLlM0#2W2YF1#"FA&k<'b0+e4hA3)-!Q#+P>Ztf=0'K?=lYoSOuWb"I3roo>-e692$1BDkV<OKVr(A,^kp#+qS7ZPqFRopg^%0"n&:U894k]8D:d>b2GX-b*.&)G3fXLgo]'*sC9:f#*5p?:C=.#\=J4BAHDgT('gWdo*]&b8K>FgUp\NG&B]_%mkVsQ0iiA?[H`"eG!k%G7>2bH-]l@GX*eTH4j$LLhPdZ:4XP=;4S-(YBQu$l7()D:'[uGH.7)WT6d;`!M9<3+ldbJ?&d;_RY7f/^:>@VK!^4f4g>;G;jbrktWUZ7RmB+GdD5'>>m/\bO7pUoVa*n>F`k^HKE8WV3OY2blE!ZEM4amnVf>aYEljM\hL~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1216
>>
stream
Gau0CmrR2B&H0m]Z0`pm3J?MO-BjijEnjBm1sHcaC$0NS.%jBa`cbq0-.D,YIOj*%"1U3?#7LYbC#;+f[qdB/pn[b71']cLU&-nTC-m.Qe0[),"6bNBcRi>GFA:a!AjF1R4@0-/^iamiR',],%M6fBXM=rO=R#orf"sRa,tp;=&13c*KR+_*q_uf1TKO,E13UQU%ej\'nX(]>=9=ie6'AkX"b_(2^/T(N_WO*RQ<Ao0%K&P$Hea?d(Q^=dWj?k*Ud;BKE03njAC_kWp%0UHGjbde\(24],;WD3;-,"626rod),NZYg,-JPAWo+5M1dk"&PP;RPiS"H0c)tJ5(;*@IK85Ria.n$\UZ-d_#'<Ifl/.09^X*Y3t@ZLn+]<E)M#50's&K*`PM9CmZ/-'i85:V>f6/MfH)c8"?O8CftKh$>R%^9i,JZna$#Lf$Na%SV+F4`q<A+@f=^1;]DBlg,%fGK#kdL0jTK[JrCDf$$Us`PAL,]7Y0L_&7)WtPT'L2P*btj83Ng+_[`_[EPDsJFYs\D:b%F<e)jLdp7:6^cF-%hPimE9u:3sN1iQ78$l%I%;PuXK#"*$>e>@I2\g9St\[[rRI4jAjaYYscM>GPNt5D8'tdJ;_N`HXAR3]fPFp;`qPK@<UoApLT7I7tHO;]:Op6q8T':1*+tIuCYV\!Q.,rK!k&4\O5!F3bpiT<O&,+H?ZEoppE>)W'a?&o/6<@=*AO=6r8M?>Z?ien`YchK,r4a\:5$9H131+AS;FWMID+)?-iU9M%TO399tif\0RYS%<]Xec!Vs8$\bp^S#'AS?r)>^9]E'K'DHWpP,sM*G8g"?IpkTo5/h'W!t:i3C]:F,C1oFlTBpr"G_7RRV/U]n3uSr8UaO,`E0Pako?;\,hr!G]j8oC=Z]BjDN\H@L"57HLuD=tF2INR(Tf`.5MPP2[Ds/RoJLBu_*0An5MH7Y+*=AUpVdTBM-m_BTRYXTIf89%_5Wnf0at8"T:(8DSo>rFfnH3<`s`p>,+E%*%;]R$Zi]=OCc'R+GDmd^U2H2hq85B<WCh?!iiN:uZF?6\dKKH;jPlKa&N=Z2i2Qp-n+[Bf=Ma=WQtm]*Yu5l5e;Bd2Cp[O<[XX+14ijJ8g[gV@$U-4gY,FI%=^csIR)oFHVY]6A??q-Ps&3(pFfts-7$NK+Io2=Ocf9XZ8c0NNi0%Y$;6Z-5ju6Q3jM0(r[/L/g169l~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 967
>>
stream
Gb"/%gMWKG&:N^lVL?=aMcjouVlcsiBc<sKPK%MaWX(-^&I,nLRms[_]t4Y3LAF5G2f5A5Q3T(dS26Kf/-;)bOSJK=!!bmTImSZB(l!PL`IQn`3#kP#$'dS(9sG\_6*P!iMQ5nVHVQV-\j8jX%:_sq.WR'C9hVW7#)_P*)"NK&ci@4#K?Y:TB@P*).42Qn[KgH/be*Lf%-%X@&^DOl+Rl>-d(BIu)]3`YkL10Q<-s0s%GTgnaQ/.$$]$T@9cW6?R,n6(M/GWk^k0%?8rf[irZ\6FLGFfBr@Nc?B'+hX/1/kiCfP\PBtkZ[/5Na<i`PK;D*ZRUKI?9?k$Q"a'q7>jR`<o5*-;@'/LSVZ6rrq(.loYZOqNb[PYelF]F&W_+\>2n>(e:SrTZlB^KfV3L1fG\_atce4ntJn-qtUd55+5HL@Z-4KH'hf\FK.%`.r;ppqk8jVVH%Vb&1hl-d6.$0mQh;If'&;\GcL&&%mcD0/n?KjIA!/q#E11ciZs48f7*mi8&\kKj*CR?*5sjc\3n$_AAlQ't:LOU`@G-V[]_F%oQ7?QH*kLZW#A<"IM#2I=aV>KC9k`/b`B$^"gY"&1(B[#ecW"SD.bb_`G:U"BQAhE]\+g^8W("(b:<`H5)./.7-`,CW<hsPsHXfP03dt9F.L/P/-l^94,%!8Ln))!+7jiA'A-#1L!k$e'M116$1#5DqOHm6\`!hi+_D;")mRs3Oh-\>WE,qOAj$]]l*&L'Tu$-D`rCF&"Y@Ln[O,e"V#H@_/@,"h%4)q)LC\A.L!n?VPUj?,l9R'l8lP_c';0qRTePILQ<e*Q22`%R*3ejNI3jTC=R%=)oZ$Q:o756jfN*a<bJ#e;/dt`B@8J0p`I<*XPBhIIp'Oi$6Du@E4nsm=hmg$0>C;?5[=+kW%kIYTSS*eL?X-[,($,%J%)bk?/!NfWl3A#ZV=V^D9#[NKPXW3D302IdN[UU~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1211
>>
stream
Gau`SD3*C1&H9tY(nL(^[Lu0%pKY>\?17V:mBq9mf.Wsu9e_Zl32pDRmB-0P*BT?/]G+Dip&r7<4e?q2T(.%J@,m%+h]rGOecY4A!TH@_"N3)ZkZu!q?KZ*Z/0tD=$t:S2&-g&uE=h.jgbt"K'*=Yu(jAe(C!<70*)/CX!c9,Z#)r;>Q9`XoqMl6+X!21&*AaX]\7,N&+tj>L\3,E#([+"[JH!C2/l6Gm#-)7f;jN9DltI8mZG.J>)#omh.N)op)R2J7d'C"m3`B)2bIs0!")(_PU&OF5CnJ(D)9%a*`iqJ;HeC-%R!6b6Yf"P7bn4I1Tc9u7(\qZra1u[^&.G-ndY?LU&5<@Kl]AUf+,WUH*XN-30P9m]aO&NBN'rfG0A8kTG:Q;k(.O;7oB#OQ4fAYAC/DrX99sL1BYmeQ/"$L^/lFlM.H0Vs+qd)qF&3rjfD=uuNT_pW5c\fcJ^KE(1+g"2_7n;]K:?(dFXHGrP(HaJ8fa+:TQ?RE-:gbTj5\Vp->.oth`+C/8&LO:(oCArA@/And%$Z9!]'5)Am#=Lf4us&]t`qZE(ujNYU$O+FfmM;'jPne75fgHYS3f%QN,"jTXT#I'Bc:'Ehc?u3A$A&6VT7CQHhTU$&SBbZG%2Tp?c*02:LLc-A%6&Da%B?_>:QBs1J+U&\Fr*:!leACSqqUrU7g/Bm7shS-K8I5O4pn:0&PiX:]Z.h(!%pNZYuQ?5/m;is+dkgF_@\NPLBSM6LkDRZ.(6p`@9si>S/N8J9pgM>3RgkuJ1sj,MgH:%XDj92Y&aQbWZd1PmbG+#8uL0kA6_:#N$"";I\+=f(gn4rP^\$(b&.e:<G!4MlBu0ms[iHb"0LW"D!$=&I5mLI/9_Kt%dtY%t57H!mdU!0?:M>9T@V?VMFsRkSJKN#<R+@(bI!6CF?`TVDf^L9WDB8guH!\U=:gLU;Q%1$g\UV#i2OVRh^ff#Mn-km204&stTIpN,Xg:*X^%DWBJu#_+R#dgRkn39Bcd>fZHed-T]!CR]JaL)s+PpEs/=p$,OVdG3X<pPmGsI`_2cDFO%5:+4@$X_Oh,lE*9*[R+gfBlF3)NY>p.@sn%@7q04F3"^YYXpP$dnhn'1As+0hM3P-9G_eG\W+ot[,nr6_Cl9rH/mgg2_<MN0Mj6#uL%Q.JS22mbTW]D;<0UV!OP#X2fT@CMRc$L.A^%N/:%foJd.n.mjMH!~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1369
>>
stream
Gau`RD0+E#&H9tYf_,tRD&L#2foo19j*!P)>USH)gL8)H`#;UN4*#P43$/+om[XKt(i6%V>@9FPp3*'Vp:4J^d%\!d^69PpW!E2agL*+i]*@#r4Qr?P8[%Zp@@*:2@g.u.g!KHIQ5p,@mA&F%'0'jT#KmQ\DWH<_\bZJ=e0(CW8sip[+WPGp#lT/Uc$+s]3![g$:#eMj&NV^*L,>N7Qm5NXOR5=%0Bg*s?p<"c'-Pn0"_>++Nt(F@3ZtU+6oj!KBAW^#9N?flp]:B4ll+aN>4M!gq5*+i1^HElBi#nEZXCue%)?fci.3&/(eTuoV49ig=b9RP#B&J.rW*p]1LI$gL8fb:%]PTu@2-`jjp:Ja49WXnL>rOWkh@1lBem6W=_>K3g,hQR'=*=#X;qbg/=iGqn23q[MenhB-S#B&8#]HSlN6:6ZAajOgDeFlhYn6u11Q9G8;0&qaJu$n89(R.NHLQG0HDaYCUAHZQM+^(?mM7<;NqPK$@!$nV:=*Fb5mV\"KMa[G%A!#2qY,>hldGPY:2<VPSZ_,*>meF(CZc$;6Zl0%LfqK@OBR<h/5T*A)X]G7<*rh:SpL4,YP*Pl[,,j)_k$nON!:g)A@Zi,bG-DOCgQ_73#5dqkZA6k4t6?.?rVAcG_4<5#Wm,43$r:0Lp`@jgUpTR>*FE2HDetc/o`\cc5sG%YkqI@t@iNN_sM)o:0>CP;,n*/B-:IMi*a)9&t8#phjc!I`pIuMjCh/ppViMX5YR)MhJh)[,EB8*f,4n"]6Y4/nV4TlVSa.!>S)8jhprkl'I.d_n9DH6)u#PYeC``#(*'CGbK&0ZjirQ$&#'4-LNg9E#0^^XD<u1dsR;-_c1EI(UPdG$iI+qnQ^9Pn3TQ`MP<3sJMlB>XVE[RR9;?V]11iU&BBu)V]e\<q[3W&MF281q<23_90[YBXj`/.\U-p8&<KLH%sY=r^bF^_9U&.pM?nP#i'P7#,Q_J07:"&*EY$cRk.p^<g#cda29S(:<L9]?Plp-A%u+H@rYXZ=c+>P[N#V\(gr/cL-#c"W8MTfELaA![W;U=X#OX&Y7;4\61b[p!i,ob^-Jq-Pf,_6:XIUB_o-I5X"P^^t^KJn!;p//0K_ha'm)D(Nq7UupEHuYLH@48V]]^>X\gLRGT?b1*r=@n0c@8!lH@PM+9*AHKj+'NQ6Ij2`U3Np#Z7:f5h*6Si(C:#i,0O(7]H6O9W\:C(=Hp=t%[+Y<?#suu-Y$V=Lh(Lc]/58K)(g69NsM:8_Ygjps5X+arTWoT'pqP\2`KjGh_?tnHCUk1N]Xb#h-B@s<6YsbHGf^QF23eU/+fA!6e:tKV83NK=go6R)kJTri-"u533PJVb@fIr@hou4""T`7k5~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1160
>>
stream
GauI5Df=Ag&B<W);dB&hkrQtOos_hh`BdB9:"-JX[h\KGcn]HRO[YP2ljQ=tqbEBP33aL9W*:MZL%'>7`8>V].CAW#RV+_fkk;j,TKjO]:Op$pL&NBgpKO7nF;=%%`/gDo?NQ;8aH(9[G*=TegRSTk[i\qaL^2*=M>VN.](!4pdoH[fK]TBVH6gF;3-uSiL[OcbHd%G3BVR?RTK`\8.:q0K*Jd=YKXO_`h.YmaXCRn1WSoYOOu(@p;CiQLB5,4=%:<Ke`>j=fJc<DlrN;$JNL8_6;R6/NWomI45l\CjZhU*53@V79-R)ckm]ArCQss_<:Z-:Mgk%$7eja[caD@?mTljIlVj^kJQn(UM[NJZ7J+[`_#MmSTB/dN?okF0-GCD@CE;Ak>&")PPlJ:"8S+D]e%\%6YEkhJ%(\<N&BZ^"g*LjM06M"73EdN>s2?E$rS_eX*4K%hBIGk2!9kf9W"0J/,3B1+e6cTMkD)*01I*!Yk0@L=-2&-Y!U64uU=*YdDOmY%$+<R@iWWb0sbm8+AkTO-33pUl'/;0,#$8(C6-Rt^-:rod2o7G.:iPdRoVD.2Ep,A=rg5lI:&:@Z2AR,kPaHUsFOt$<`22@SSYC$Y+'12]7EmoW("4ef<#RttkoK$F:IjBPN"n8nG1Pf2&N`^Z:;AAs=P17*Fo3b2bCH9+?lun"*bB"UWc0SW$D1lanb#h\QI:p9)P)?(:aRA++bGP22N;80h]o$mA2]Y5[h'RU7mA/_;IbfK!)Lb,MQV=_0LR_)&N:#6]3oRc4D_.iX+I(8aUS\[>h$/G('j!mu/F_T>b.[\j+IEEpYLr>)92a!,e5FBX$a^etF]1p*)+7*XUmu=aRmR>3*WXC&1Rj,tAf%REfPUbNKIa!?>mfY70WgB$XQfLd\C+:(&#Jq[q_QX+lW*jaOR"k,nUnX18^</R9u)n4Kag&t;7_J4\G?.5=)9U#Ekc)(o@=%m%tkB-AZolsOsM_?c^r9YIGTu(arGH300WPC$dZ*Q<^VE=TJY,O.]:Z\TR//T<EfR$8Tq."9If5`Ei);dN%4&s/?;DM%6@9%TcGrj7l!E<1'7?VPZjI?7(-GP%G`lfhfe_\\AO[`2R9b(_qY'UY?9U^EI#k19X_`0$;qe!"[4XnSgEW'ndPrQqB%/<9'Q~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 661
>>
stream
Gat%^a_oie'LhcqMS!sK<&*J`nhN\_(,-*F(:#@$U#d)[Ys^&]D"R0h;B?J$78XA[W&L5n*uTD(AFWu*^dF-o-O,k\IfUK6"6;7#k+/sk4%>7YA<`o._g".9=>n<LT)Y@WUNAU8,3e"ZqH8\$l\>Rge+#<K]/Frr>GOf"Mf>;^qpc!o+MF7PWejqHbr7u[)s"3D56ZQZrk&H_^Q3:6id*lY'(GT,Kk5gh%0Jm1blUbZH<q?`j/G!$4t(UTO[K<[.[lmq>nO?&bGS7oUC8,%f+en-ZWiSn'L$[LB1Nt4613Y7X0OS73@TB%c=Ot=,a4Dmh.P.]N$UL[[jT)mpofj9KE'K7AE,E,ZN\'k?TcX*!%Nk5&/#@Po+a3;]i+]q[YPjlA!$rZ4_[Si<@b9$T%p<di^N+3(&m9\^l*eZ9;F05Re$u+;$e[$c:as0[2%2/F6)tDNMj6?9M#?0=&bJ2>7#@S$Vc>/oOGa=<TM!;J&+,%hX#Y.6Cb9QF)JMEm=8HkB%!.JV8X6SBd\'Xd=T\6XQa2$;2X:$FE,QBZ`%Wer//)MQAStT(WLL]9G#pB7<*Kog#\gYmj'&8<2tpr(FQN$BpF&-ARH+UFq'!-^ah9CS-b0t:>8h7:Z]&U1O?)OmO^P(^ZUUN([G:p(7j@Z]0cJ~>endstream
endobj
xref
0 25
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
0000002275 00000 n
0000002556 00000 n
0000002662 00000 n
0000003452 00000 n
0000004425 00000 n
0000005733 00000 n
0000006791 00000 n
0000008094 00000 n
0000009555 00000 n
0000010807 00000 n
trailer
<<
/ID
[<71e7c7d830850d86ed44e0355ffd582a><71e7c7d830850d86ed44e0355ffd582a>]
% ReportLab generated PDF document -- digest (opensource)
/Info 15 0 R
/Root 14 0 R
/Size 25
>>
startxref
11559
%%EOF

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +0,0 @@
# Les 8 — Slide Overzicht
## Supabase Auth: Inloggen & Registreren
---
## Slide 1: Titelslide
**Layout:** Split (cream links, blauw rechts) — Keynote stijl
- NOVI Hogeschool logo
- "AI leerlijn"
- **Next.js**
- **Les 8**
---
## Slide 2: Terugblik vorige les
**Layout:** Cream + blauw blob rechts
- **Titel:** Terugblik vorige les
- Links: Wat we gebouwd hebben
- Stemmen werkend gemaakt
- Supabase account + project aangemaakt
- Polls + options tabellen
- Foreign keys & CASCADE
- RLS policies (SELECT/UPDATE voor anon)
- Testdata via Table Editor
- Rechts: Wat nog mist
- Supabase niet gekoppeld aan Next.js
- Geen login/registratie
---
## Slide 3: Planning
**Layout:** Gele achtergrond + decoratieve blobs
- **Titel:** Planning
- Deel 1: Live Coding Supabase koppelen — 65 min
- Client setup, environment variables
- Data layer herschrijven
- Components aanpassen
- Data persisten testen
- Pauze — 15 min
- Deel 2: Uitleg + Zelf Doen — 60 min
- 30 min: Uitleg Auth functies
- 30 min: Zelf /create pagina bouwen
- Afsluiting — 30 min
---
## Slide 4: Van Array naar Database
**Layout:** Cream + blauw blob rechts
- **Titel:** Van Array naar Database
- Links: code block met de oude in-memory code
```
let polls: Poll[] = [
{ id: "1", question: "...", ... }
];
```
- Rechts: Supabase query
```
const { data } = await supabase
.from("polls")
.select("*, options(*)");
```
- Pijl van links naar rechts: "Zelfde functies, andere data source"
---
## Slide 5: Live Coding Deel 1
**Layout:** Blauw volledig + cream blob links
- **Titel:** Live Coding
- **Subtitel:** Deel 1: Supabase × Next.js
- Stappen:
- @supabase/supabase-js installeren
- .env.local configureren
- Supabase client maken
- TypeScript types definiëren
- data.ts herschrijven
- Components aanpassen
- Testen: data moet bewaard blijven
---
## Slide 6: Het Patroon
**Layout:** Cream + blauw blob rechts
- **Titel:** Het Patroon
- Server Component:
```
async function PollList() {
const { data } = await supabase.from("polls").select("*");
return <div>{/* renderen */}</div>;
}
```
- Client Component:
```
export default function VoteButton() {
const [loading, setLoading] = useState(false);
// interactie, fetch naar API
}
```
- Uitleg: "Dit patroon verandert NIET — alleen de data source"
---
## Slide 7: Pauze
**Layout:** Cream + grote blauwe cirkel
- **Titel:** Pauze
- **Subtitel:** 15 minuten
- "Supabase is gekoppeld! Na de pauze: Authentication"
---
## Slide 8: Wat is Auth?
**Layout:** Cream + blauw blob rechts
- **Titel:** Wat is Auth?
- Authenticatie = wie ben je? (login, registratie)
- Autorisatie = wat mag je? (RLS policies, protected routes)
- Supabase Auth biedt:
- Email/password
- OAuth (Google, GitHub)
- Magic links
- Session management
---
## Slide 9: Auth Functies
**Layout:** Cream + blauw blob rechts
- **Titel:** Auth Functies
- Code examples:
```
// Sign Up
await supabase.auth.signUp({ email, password });
// Sign In
await supabase.auth.signInWithPassword({ email, password });
// Sign Out
await supabase.auth.signOut();
// Get User
const { data } = await supabase.auth.getUser();
```
---
## Slide 10: Zelf Doen
**Layout:** Blauw volledig + cream blob links
- **Titel:** Zelf Doen
- **Subtitel:** Bouw Auth in je project
- Stappen:
- @supabase/ssr package installeren
- Auth helpers configureren
- Sign-up & login pagina's
- Middleware voor sessies
- /create pagina bouwen
---
## Slide 11: Huiswerk
**Layout:** Cream + blauw blob rechts
- **Titel:** Huiswerk
- Opdracht: /create pagina bouwen
1. Alleen ingelogde users kunnen polls maken
2. Poll wordt gekoppeld aan user.id
3. Test: zet je eigen polls online
- Extra:
- Google OAuth integreren
- Profiel pagina maken
- Dark mode
---
## Slide 12: Afsluiting
**Layout:** Blauw volledig + cream/roze/zwart blobs links
- **Titel:** Tot volgende week!
- Volgende les: Deployment + meer features
- "Je hebt nu een echte app met login, database en auth!"

View File

@@ -0,0 +1,552 @@
# Les 9 — Supabase Auth
## Docenttekst
**Les:** 9 van 18
**Onderwerp:** Supabase Authentication (signUp, signIn, signOut, middleware, RLS)
**Duur:** 120 minuten
**Vorige les:** Les 8 — Students hebben Supabase gekoppeld, /create pagina werkend, Server Component patroon, polls database
---
## Leerdoelen
- Authenticatie vs autorisatie begrijpen
- Supabase Auth functies gebruiken: signUp, signInWithPassword, signOut, getUser
- Server client (SSR) vs Browser client onderscheiden
- Middleware voor session refresh implementeren
- Authenticated Navbar bouwen met getUser
- Row Level Security (RLS) voor authenticated users toepassen
---
## Lesopbouw & Timing
### 09:0009:10 | Welkom + Terugblik (10 min)
**Slides:** 1, 2, 3
Ik start de les. Korte recap van Les 8:
- Supabase project aangemaakt
- NEXT_PUBLIC_SUPABASE_URL en ANON_KEY in .env
- /create pagina with VoteForm component
- Polls tabel in database met votes
- "Na vandaag kunnen jullie je app beveiligen met authenticatie"
**Planning tonen (slide 3):**
- 09:1010:00: Uitleg Auth concepten + Demo
- 10:0010:15: Samen Middleware + Auth Callback bouwen
- 10:1510:30: Pauze
- 10:3011:30: Zelf Doen (signup, login, logout, Navbar)
- 11:3011:45: Vragen
- 11:4512:00: Huiswerk + Afsluiting
---
### 09:1010:00 | Deel 1a: Uitleg Auth Concepten (50 min)
**Slides:** 4, 5, 6
#### 09:10 | Slide 4: Wat is Auth?
**Vertel:**
"Authenticatie is: wie ben jij? Login, password, je identiteit bewijzen.
Autorisatie is: wat mag je doen? Wie mag polls maken? Dit regelen we later met RLS.
Supabase Auth beheert alles: signUp, login, sessies, JWT tokens."
**Demo:** Open https://supabase.com/dashboard
- Klik project → Authentication → Providers → Email
- Laat zien: Disable Email Confirmations is AAN (sneller testen)
- Zeg: "Students zien zelf deze checkbox na Le 9"
#### 09:20 | Slide 5: Auth Functies
**Vertel:**
"Vier kern functies in Supabase Auth:
1. signUp({ email, password }) — Nieuw account
2. signInWithPassword({ email, password }) — Inloggen
3. signOut() — Uitloggen
4. getUser() — Wie is ingelogd?
Hieronder toon ik hoe we deze gebruiken in Next.js."
**Code tonen (slide 5):**
```typescript
// signUp
const { error } = await supabase.auth.signUp({ email, password });
// signIn
const { error } = await supabase.auth.signInWithPassword({ email, password });
// signOut
await supabase.auth.signOut();
// getUser (server of browser)
const { data: { user } } = await supabase.auth.getUser();
```
#### 09:30 | Slide 6: Server vs Browser Client
**Vertel:**
"Supabase Auth werkt in twee omgevingen:
- **Server Client** (Node.js, SSR): via cookies, secure
- **Browser Client** (React, CSR): via localstorage, minder secure
We gebruiken @supabase/ssr package. Dit handelt beide af."
**Toon twee code blokken naast elkaar (slide 6):**
**Server Client** (middleware, Navbar):
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch { }
},
},
}
);
}
```
**Browser Client** (signup, login, logout):
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
**Zeg:**
"Cookies zijn beveiligd. localStorage in browser kan hack worden. Daarom: server client voor getUser in Navbar, browser client voor login forms."
**📌 Slide 6 referentie voor Middleware:**
Middleware zorgt dat de session word gerefresht op elke request:
```typescript
// middleware.ts
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(...);
await supabase.auth.getUser();
return supabaseResponse;
}
```
"Dit zorgt dat je Session JWT token altijd up-to-date is."
---
### 10:0010:15 | Deel 1b: Samen Coderen (15 min)
#### Stap 1: npm install
```bash
npm install @supabase/ssr
```
#### Stap 2: lib/supabase-server.ts aanmaken
Voeg dit in (hieronder exact):
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch { }
},
},
}
);
}
```
#### Stap 3: lib/supabase-browser.ts aanmaken
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
#### Stap 4: middleware.ts (root project)
```typescript
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
await supabase.auth.getUser();
return supabaseResponse;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
};
```
#### Stap 5: auth/callback route
`app/auth/callback/route.ts`:
```typescript
import { NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase-server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createSupabaseServerClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(origin);
}
```
**Zeg:** "Dit is standaard Supabase/Next.js boilerplate. Niet allemaal letterlijk begrijpen. Focus op: server vs browser client."
---
### 10:1510:30 | Pauze
**Slide 7**
---
### 10:3011:30 | Deel 2: Zelf Doen (60 min)
**Slide 8**
Students bouwen nu zelf:
1. app/signup/page.tsx
2. app/login/page.tsx
3. components/LogoutButton.tsx
4. components/Navbar.tsx (met getUser)
5. Uitloggen in layout.tsx
**Reference code:**
#### app/signup/page.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function SignUp() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const { error } = await supabase.auth.signUp({ email, password });
if (error) { setMessage(error.message); }
else { setMessage("Account aangemaakt!"); router.push("/login"); }
setLoading(false);
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Registreren</h1>
<form onSubmit={handleSignUp} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm font-medium mb-1">Wachtwoord</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded" minLength={6} required />
</div>
<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..." : "Registreren"}
</button>
</form>
{message && <p className="mt-4 text-sm text-center">{message}</p>}
<p className="mt-4 text-sm text-center">
Al een account? <Link href="/login" className="text-blue-600 hover:underline">Inloggen</Link>
</p>
</div>
);
}
```
#### app/login/page.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) { setMessage(error.message); }
else { router.push("/"); router.refresh(); }
setLoading(false);
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Inloggen</h1>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm font-medium mb-1">Wachtwoord</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<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..." : "Inloggen"}
</button>
</form>
{message && <p className="mt-4 text-sm text-red-600 text-center">{message}</p>}
<p className="mt-4 text-sm text-center">
Nog geen account? <Link href="/signup" className="text-blue-600 hover:underline">Registreren</Link>
</p>
</div>
);
}
```
#### components/LogoutButton.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
export function LogoutButton() {
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleLogout = async () => {
await supabase.auth.signOut();
router.push("/");
router.refresh();
};
return (
<button onClick={handleLogout} className="text-sm text-gray-600 hover:text-gray-900">
Uitloggen
</button>
);
}
```
#### components/Navbar.tsx
```typescript
import Link from "next/link";
import { createSupabaseServerClient } from "@/lib/supabase-server";
import { LogoutButton } from "./LogoutButton";
export async function Navbar() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return (
<nav className="w-full border-b p-4 flex justify-between items-center">
<Link href="/" className="text-xl font-bold">QuickPoll</Link>
<div className="flex items-center gap-4">
{user ? (
<>
<span className="text-sm text-gray-600">{user.email}</span>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="text-sm hover:underline">Inloggen</Link>
<Link href="/signup" className="text-sm bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">Registreren</Link>
</>
)}
</div>
</nav>
);
}
```
#### app/layout.tsx (updated)
```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/Navbar";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
export const metadata: Metadata = { title: "QuickPoll", description: "Stem op je favoriete opties" };
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="nl">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Navbar />
{children}
</body>
</html>
);
}
```
**Instructies voor students:**
1. Maak app/signup/page.tsx — form met email/password inputs
2. Maak app/login/page.tsx — inlog form
3. Maak components/LogoutButton.tsx — knop die signOut() aanroept
4. Maak components/Navbar.tsx — toon email als ingelogd, login/signup links anders
5. Update layout.tsx — voeg `<Navbar />` toe
**Ik loop rond en help. Studenten kunnen stuck raken op:**
#### Veelvoorkomende problemen
| Probleem | Oorzaak | Oplossing |
|----------|---------|----------|
| "Module not found: @supabase/ssr" | npm install niet gedaan | `npm install @supabase/ssr` |
| Navbar toont altijd "Inloggen" | getUser() returns null | Check cookies middleware, browser dev tools |
| Login werkt niet | Verkeerde credentials | Check Supabase dashboard → Auth Users |
| "Invalid PKCE flow" | Browser client misconfigured | Zorg dat .env keys correct zijn |
| Logout werkt niet | signOut() niet wacht | `await supabase.auth.signOut()` |
| Layout.tsx error: Navbar is async | Navbar is Server Component | `async` is ok, use await in getUser() |
---
#### 11:00 | Check-in: Navbar
Ik check of iedereen Navbar werkend heeft. Zeg:
"Navbar is een **Server Component** (async). Daarom kunnen we direct getUser() callen zonder hooks. Dit is uniek voor Next.js."
Toon: `const { data: { user } } = await supabase.auth.getUser();`
#### 11:15 | RLS Update
**Vertel:**
"Nu authenticatie werkt, beveiligen we polls. Wie mag die maken?
- Anoniem (niet ingelogd): mag zien en stemmen
- Authenticated (ingelogd): mag polls maken EN zien EN stemmen"
**Stap 1:** Open Supabase dashboard → SQL Editor
**Stap 2:** Voer uit:
```sql
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
CREATE POLICY "polls_select_all" ON polls
FOR SELECT USING (true);
CREATE POLICY "polls_insert_authenticated" ON polls
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "polls_update_owner" ON polls
FOR UPDATE USING (auth.uid() = created_by);
```
(Zeg: "Auth.uid() is de ID van ingelogde user. NULL als anoniem.")
**Stap 3:** Test in /create:
- Niet ingelogd: Knop grijs / gedeactiveerd
- Ingelogd: Knop blauw, poll aanmaken werkt
- Na uitloggen: Weer grijs
---
### 11:3011:45 | Vragen & Debugging
Ik loop rond. Studenten kunnen vragen:
- "Hoe debug ik auth?"
- Supabase dashboard → Auth Users
- Browser dev tools → Application → Cookies (zoek sb-*)
- "Hoe reset ik mijn account?"
- Dashboard → Auth Users → delete user → registreer opnieuw
---
### 11:4512:00 | Huiswerk + Afsluiting (15 min)
**Slides:** 9, 10
**Slide 9: Huiswerk**
1. **Google OAuth (optioneel, moeilijk)**
- Supabase dashboard → Auth → Providers → Google
- Copy Client ID, Secret
- Voeg signInWithOAuth button toe
2. **Profiel pagina (les 10)**
- app/profile/page.tsx
- Toon user.email, user.id
- Update password / email form (kan les 10 zijn)
3. **Maker tonen bij poll (les 10)**
- Voeg `created_by` toe aan polls tabel
- Toon bij elke poll wie het maakte
- Autorisatie: alleen maker mag aanpassen
**Slide 10: Afsluiting**
"Volgende les: Deployment! We zetten je app live op Vercel. Daarna: Google OAuth, profiel, meer RLS."
---
## Extra: Supabase Auth Docs
https://supabase.com/docs/guides/auth/server-side/nextjs

View File

@@ -0,0 +1,263 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R /F5 14 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 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 /F4 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/BaseFont /Symbol /Name /F5 /Subtype /Type1 /Type /Font
>>
endobj
15 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
16 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 19 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/PageMode /UseNone /Pages 19 0 R /Type /Catalog
>>
endobj
18 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260331161655+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331161655+02'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
19 0 obj
<<
/Count 10 /Kids [ 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 15 0 R 16 0 R ] /Type /Pages
>>
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 367
>>
stream
Gatn`h+kg@(^Api?H%8IB%6fuo1TfT-D3Zi"Vb!G7OJ!uDr5Hj&d[EPD:7mGDu2\33EVZ]>N[Npf&GtdK+$<jE((N.#^;T!K^Y(%;5oL0Lc(raAh';qPG8/@oRW0%@771RW(`RP"_]C?JSIJ;;r0Tj4S2k^Kn)"^6AX28GT:VJ1*g[j_8t5!04<[G0/;8>%,AsEO\++'M,`]<[8]68FL--H=*r_=R\_[+I.!I$ZfmDGnM<Y:W",D_cYB9?"!qC3p*!RT_;QVApQ6J@;!3brFWI0XO2\OkDjgsl4Nfqd\#Xn0-hfK><J6^do[eNrb70`"`@%oO:&;XI=LjK@n:e&Nj0NMlNj5YI9*clg!=IlmkuaGJ~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1444
>>
stream
GauHKD/\E'&H88.0g2KN[)@4f%TSc@3HB<q:!s#9.p7`FZ]HKQbO$p$<L!O?Ymh1?i8U9%)@9I<XIj-sH.db!M'%n/7l,[Z4uCRk&.A>E+HGDG^C-V3_<1p+[^7'mG!*eI5A1q;A)"L(*ugMYq2Pi;*LM#n!i'>)(Wa]!^6<BLoOeG<!=:N;+9e^WmrZ-+g+_GGAeR.1kZ^"]8]9b[-$AEIU&SUpj%oU>U>jo3U,j`gh$/mH(mO2U$EeP)8&k4jC>^T%GZ985fg[m+a3S^p*!g[G1E\1O,)-glY(Q$Te5A,hBHC$q)2,E`Nu)4FBVJQpa:X5R)cl`Aa:am2kn/#LPfN8,bc+JjZtM^C0rIC=(bD*9A'S$UI_5$#(3d9$_Nuff^d9a^@Q7q]hKWXEn6q<Q05LX/2arm,bsjWkLNVWg"h[n%70Tbu!i/R!a]7<jEW;HQ(lK9&^m)Z+YiH(md58Nk(NHNqFdLs4cS\6O@53)>_WOV1<O\=Kn-F^rj$V`D[Vq>B$0HN/0W:FJ7AR4E3('qeP,WU2QB&XIijlk8XH,,T@Y+%q^7XR^F-A>Op`s"diJ26c92qTt\QeR,$$6C/@nLa'@*fNI@5H,N%!j2;*JPuDQ[h$n.e#k]W0a2_Cjd%k/iODQ=ICcBkE@B7Ik5'R&'$NDC''8Q<<CRUWut_ZR2a^@#Vh0="$U]&EA;80dudKSPcjC^b9660Ec[(?(fW)r^Gn-b;^!$K/WpBd9o0!l>(L+/QF3'Pj'.4'<J"r`%H\s-\P@_OrM=%7+_ObaVnSsimL1:/DLM.:&8VM)&rG%U%noShR0WKjk_`g_<U7q@Ufh6h]YYT@jbH4N3Zc07cVO?(@dA7C+m)iO[*6DF/L:7Y>]Y>*WP&pbRT>">J'%MNj0qWr[qm:."+.5Y[c\G2D^"MeU-og(SQ5bp:!\7Q+cW^,7N[C&T':Yl+6"%?<V;b64i">fAUUYG?\#jb^5](gD>_Yu\qdtU[q8`73\*AhE9c=2S`L*28R-b[JSPu)Pp[k)ELMTXJeKS6WL'M;a2+I&fXQ6<nX9PtNL9n-)>EGdbbe8nXLE20M/Zd:^lhVuO"Ur%(F*Nc<An<IJE)m38#)HRW+=.o\f=W$m.IMR9VN<m^6/1hM0:WBiWKI#Mgd3$)Y\2e^k&Qf<t%TTAs@-s<.)V*]ns]2^XJ,n1JM(c[b?5D'25-[6.Hs3q67DLoMB,81N]$&cWVu.;iZN5]*a_Ik!Gp[RK%EP;eR2,#8g)gK6L+cnZF:YXhB!4'cReZ75o-&*AON7mB\tB9=BVUJ!1/Xfi3TVobr2eZV;VqICLpQ!O'.kjHsWcYbHDZ7e5qiO_p3hs/72)Hjjddm*]Z%7>0?+Y[hI<GQ.$f>RjN?T2'<NK?N;=j'Ln\E7OoinbR+Ck83;?^N]\1Z1n7NS_VQ8S+bn&/1'645K=L.!<~>endstream
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 581
>>
stream
GasalIr!dm&H1LYi`[0YUJkVpp/8oMP2a;[aKes"%1GPmPh)=@V398DP^N4rm3HNeBB^:,+ME-rlcFc#IU9^b"i-3epO`^B%t"C!,7O>Q,4=5Ih?HOhTe'^tKHVWBUD3Zs(DAA:8f,?Jq-tVUE-aBH?TZ?=FnK^I+n,PC0^.+3r$m4):[D9=6(THAdXcbld]RZuWD^Jd)-`p`H+8>+l\c8BT>4eTd[1KFEA_"?dT2T&*@eDlmg2BnO!tC$*f9Lno5X5m'SX2M4-?^c;bNUZd_Hd=^3t3gomsZQ'+7@d2Su82OpcX.?d'Ed@FH03nG4R$88h^mpGj;8hVFi`^9e4rn48UHZ:[[T!'f*:DB7_lrbE8LTm]&qJR$XGSnP8&CSCR&\OOfNddh52263R@$R9$'[JFa9-J%PFfU>4ddp3U4Q@$C(VY7)n.W)H\WKtED@?\Z1T#oF?=;G'TE;k<UA_.cI^2%B;;Yd4;=KKZP;T"5:hDjll2/d/H38=1L(d)dKk.9%lQ,XA<hN;gMNR&s#iTT=P/=oT@_a,9eVI)i+LN5B=2AgobcTXeTc^q*TE;fmqBI_^~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1164
>>
stream
GatU2qetI`&H4hB`>n.F:?`Z]![LXsP)*)8VRc><UfMVl2J+\Xpm'Ym8Het[2iA-R_9cG*/M,PFq<$FWU-fkAIJ%bNJ4gigCE7L!lj;kkZ,"))JUR04V!!o<780alFeF(rOqZ"q;%J^b;;YdG@#(\_FL>ldFAnkFULSDJ'-LnjD4VY2://-HHdM].+--Nt9K@_ITV'@U>gX[=T^H:tm*B(S<uh4XViF:AUr9%A3p'b6OJ)r^0%O?7bYZg3<d>SiLa-:``a=;'Vj%KZi.Ju:'@+O\$'8'iJdLD2S;\Om"<rTP3/)>lr@#Kl&q\d-fGNUIOuEs@R&&?A'^tm>Zb&DN3nj&9)V5d#c[sLP2`E$\r&6G'oOMh7q`AmbbX3j+)rFLfCH[Vo22tpSg4EYQa*sDo>f%IccDY8@`mM\*J^5MH9:53.KZ:2>:$;Zo9G<":cI_t$h-9Rs5;A;=O^Q3P?PA`DXhqH]M/Z6(.glO'$AlHtOJmD`>aV=L.od,gY;jchjC`%3VZlZ$)8HtpLt>n`I,L_cO!kW9F2#pC=?<SU$AaUL"UqF"HH'Z"1t5-BlsfAp(<sqfi%KG!GCOo0iG(3^^[#8#LP+"QVjhnN?a&%qrHLF#IIMH?mX/XpUE;_s9UtK1<XUFO*s>>;q%*EH;]]dJ.T\!ObW.4uD6K;I`H!gt*jXV+Cp1,u_;lV5GD_\?-kQnCJS$`0R0j@-r4GB*/NV.>SC?1%i"!eGc%uD=rts``A[0<0EY,L9k2%%qOIargcpQF;'@N?:AS#KnP<Mp[-l,-h1oSp7a!tHX:.7-<[I&kf6Hcs;OOYSi-bbA0X/6[+4a5'D)>qt1<![qu2p/^jUsaH'fag<C*a;>%bXWA!Y6B4?3!?)(`6;F2Ogk+UW"8J8Sj-@?Z\QJ5IHRt6XO[*TnB(76d[TP`F0@ri[aj1q1sA^A,Ohg6K-lGP4Kc6j0\/3Y[^V5-6SE\sH%tNpkMQatQdt&t^GC*)S>`&E>2VVYla6KP.ji>U"1pBNp5P%__rfUq.!MT3!MR:Qj1a\H8E'[pp'fTg2c(d]62fGTj0@l&AL(7d::$Wh)V[m`"B=h0ZeD"]1PrDPg[DE'P1s'5m)>ODaCA:bq`<lWlu1@A9,1r>=2^@6b1YG1bXMDse&hs>8hS!.(QeA!d/~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1414
>>
stream
Gb!SjD/Z1=&H88.0h(Au;\^uE[Zd5PlfI6(8njP;Xt$jILBY\b"r;)Z@],OlHY4-^S:GZ@l0_1\al[/@Sp=pkFr]_ojlg&9!B?R#rQtYD#Lb[mY3!Om$8/4]T+7BnADafkma)ukkigY;\G\VUd:]L=m#Bd$=T,"4s0r<bEF1:/i;qVRG2,b)EmY!P8@piZ%QYDmQWMNqJK(,S5jfkeVZFZ9f,MPu(sh.b7=GNkIpi,*K(BpeD3.58MnHY/;S.H@WWc^ifr6_#93O/aQ=Y4\KtPLnN.kreI._c<NpG0p^:tFC:IW-r`8I'jG0ne!:Ye*<(_ATb^kIrYSd+\ma:\>068%.rSi]f4V*`$;I"=uND36Q6j`i-#AWGbT9>^tr,#,lJac7UlcF5R+<sfi6Rb#R19j1(%[4]LbZoPaZ67/IlmnpZ]"JXHj8A8n4?:Fd0f4n"*O&B-o='@Ap3@ePFo7Hk^/)4#(Ia;VsfcdG4ZTu]0GqS&/9NW#/#W^%#Fn[ggC/hO"@E]5=[:s2'c^1U:[,UcPiK^t\juJ]Z<rJl-J#-bIZ68/#f"<L$T5)YSS7B1:.$HH49U+P49sVo"ahCRq:UH`s=>1f7=>EZQ=T6*aQuu5B?coSFO_Xa`?5YbVam$.EW.e8!:^LBkl"i[dl]Hm(7uF#Qpf;"ME>u*BOOo"LUoJYGmug,#1g$i@$S1,#&]p:A;cNg6g5JLB'sFpAlL"rcrRT[<!U7Iu)*VX%R?mBZpX=_;-=].`#jb<8Vo%-Le]Nbs5uMlrL(QAuc0k/Q=t,qQ63YLuE!iIND&bt<e6Ifa?Whso@qj5H3/3*gbb^^1OK;,b/qqLh)Q+$]D.2Di)N_>4c/'q)H-2uJV(pZmPk#9<;(dRfqdY!3lnP@U-)CIIn<-=;f"3fLfl`KkbX_9@FSDH.@b^o_9>o&lGh9)7Vu@_#+S)=e+i,9N&f$+1SEff'[^Ft)Ce$T<,fkb"bj'4)i1@mk:=f8U6XcQ><#nZ`<);U&WYu_fYTsl*8k\Ko%\&1AHo86tR6C&<\uB19Jai^BK1b*$&IAl]IXsKU7HV;fYCA6U+dXX/rGlqZ9jI9o^">hpe`smib=TXUOG@j2=V!GuKlaGZOq,>ai`pC4"+hq6?Au^2]ff>$`o"`&Kr.@kS&9-Q9[7Yjd6Xp%g;X2PoH(tC,rd*bb3MVkRkU!9k+MUg/l\m'#OfJg_geH.Z7H)Q^t<?.`d[8a]8V27c`gEuB`GNtG%c-l#0,=K!JrYh\fUOZ:F?lO7AlErAo)+Xm@[d$C2ZK^@R0"ib2_DCN:h"oTL!QqFoOp0XV4]H6tJm8#_g:_c^[s$eQ8$(I4SsS%kU5ZH^`I#lTr!sbj@JmJ(/W7%cU6-:^_Z16!R@["`sH(j6Ib7d2_Rts-I6*AdJtH"6fRN[/~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1156
>>
stream
Gb!#Z;/b2I&:XAW3!`XF%$P3^E]0`%$`$FH!a1X3#5U%BJWe:k=,Bm*.X#G!^[F]rYnP5npFou3CeN9++7Hp**YlJH5(WZl$\58]p`BPC`irQ2&:,KNDW#gqpc8]SQKOrc*aXL90V1tN8U,\Z\A8]b$<L+?9P[=>R::f22-9$m(gCZ-Jk8Ll"g&BR06W2W=^]=/;CW,7>^V*fOp*fH2qlUBhKaGPMn`Zi,FIp22qBb#'ruBbX'3H19*$t,j^L5TT#%kC*s.F(MFtc.=>6?H";U3;F7gp)TW[cj1QBSOj"/O5p7Y)Z<GSQH$bY<QHmcA<Wa[D\k\n`=G9V+M&h[(?]P4O@,?HG"YTKrWX\gt,GP9a(dUu_5-&YlZA/d$EhY[8iQ'1G?ebbl3d*L&l(c!G$*3J]u_Q$*<_fCR"E/-!qh?U'9lNb=I#.+,`[$ZDNWR)L[g*F#jW>PpQQ:%e>:>C2c]u5?)*^&9\.O>hrN5'/J%k;h*esTV0<.#CP\^&o>08G3pC8WWff_$i3=!i+.DG'Dd7s<J<f)oGif9Lq#8uO(`?<L>I"lc"Bg,[ko$I*ZW_6HVGjg",`;5(.X+LaFKdtBqNS5[4q=Fi8@mB5_:LOsBu[9FnQCg>C]6Kqf9o+'J0AAL!OVVUU!2`[t8cu(+MieS$<UW@#8U6td[E.Z4('p.FhNXDq%gS:e'*A:9T/4TB]%8V5u`c_-W[E)L(q(ZfPbP2It!32d`^VGNSKo$pkk-gmUI>dt,2]$&-5sLZ'CQ$>d#F<t$gY07[7JWOl950!XVV*kEa.`l91R4^./7s1Y/V9=aS&*Mn6UWe*/V<7[,a^Pj#%)/K0EM1giVZcBq_*'ibi2=",rtkk`GN*LPJ6G&.>\SirVrs><,52u>n^^ZpO`PSY?7$!1>D6CNRp>]H5i\u7%q1.UJ[58FiH;m+SOYZ-6H&i[Y@#1@Ctg$Ui"0FUH9@,ZL?"Nh1Kq0>iFR&ofUQO95rT#i1+t<l8PE/kRbIB&cL%aqmHlNhS?K\NkWkXpf)_9T<`ML+f9;ePr!=bG#Q/GpNuZehNmG;Gb8LR.A?eXa/$4o#?1LHPYoo%3PIM=lI+qhe#09)kqT/8oYuR0r!lY3Bna\d<^V"%\?c")V[/@0Os^S"V==3[%/p9pBBc;~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1392
>>
stream
Gatm:D/\E'&H88.0sXba;Oq\&&kG!T<N9h7.@900mSd%aCO7e]R9b#\9gFO7hUsae3uF.Sc:B(joCC,n4O:)6*W10E4.ZIXNPYSD"4)bm"ME(f@=/M+a!^nWGT4gKFFp_9G+j*t39O]]TH>dJIb4%BkMQIZS`%p$k;(BpM"Cg[HJ:7tN+\Np#;<#RkQIe<T,!knVPJR`;dQK);CTm2,o:2JIY@6P)T6:dZiE,TH!;CUg^6+$@RcTVaXn+cUgqW<0Y@njSVmhH#T!]$Dh,JN-&">)&B_WJbbl>1'+.bO$VD,H0+/C)8Zr8HP#V?gXC2PO_.R0Sm,hI3b)MB$<CYPprFhS`%SNX-lc\KY\W"b=Pc0PISWe&h$fa2q=fb\u^/GQ?%qi-#joBd<cD1M=AJjj@C"!SEQHtZ?p.1a0]VgE/3NXE4"<s0`#(pYMH3$](jG@EZPR].me<*nVr[:u+kbkGu.7uUK343#WL^eNXT\Ab/c+<^P:fRjW+SPpH,."8a)&=6B*Nn\XXg(*HPW*,BLR,djZfMV>D^riVmWLWFj`;pJ]6BU4N]#P6U7X*,b'BFR[ZA8,5W&H)-*8-9BHp!uF"Vd4k!f/nE;mNL*=7*\jq/2DUTX(YSe8W'\NqTZA+#(Pf,=HhB2aijgW7u_nTubJY;'/uS$lFA,&Y)bL6ArC\RN[Hd+-D^UiMHf'n$SDE/:>@8;JJg8pn#6XLH$7>YM-n,29Ka#\d,FlI:'a-Y8l:`'m"H*C4+h0ZiP\*_WK<ajQ3JHD6WiVW^_-0uCrtD5YLk*:";#A;K$u,N'c$$"qD@Gb/$4cR!W*QKS(ZeSYU9-<UGQ5upJW.7(lWnE=Lr[d^3'X_8t@*+<V,$c64M'l.>83cmisHp)GVdi2aTRaXFf0K?@u:"fOf[FS+dV(Z?Ua,-'_$<f3$*Fpk37_c9%-]ah__*!%LPXTA^.(,h_Udhf;];I*$!>Go'J7<5>HDhsDA:s+!]Vj*9Ni&4DJr7%R3ki5lp,7&u:\WZLqNgU[P6#SdNo(.i#=jDjiGT1>FZ,>UE-&>'0%<5hH`/JlKkU$rpA0='<]L'X1a+tDbJOu[/IRF;pk^Jp`NX@XC/ala-m%sVlK@V3SFP`a9ad#OEh;E8h]&IYRe)?rdYXf:MdC+HD<C1ZdMJ[R'(T"s\L1(FIN)f.V^n,JOc_&iIB[PB#bj&[-/)Mj;;i`kQ0cD\<fZoIB)Mo!HIpXjRkfc,RZE/piVMm!lZ98P1u"NcIs2Yqg_#6A,+W3oitskO1)=#dV^/G#UJ(bCi/tm0bdkeC17$rMPud:8GOSess,Wad]lS#48a(hlH0o3ma18c>*O#2hSn<8")?K.fgej5RQ7iKTfB-YG*:nTr'+4&&r<?ZPJ<PH`.e;!i~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 317
>>
stream
Garo=;+ne\'SYH9.h4pTo:^$P=<)3B#l:?,@NP9JgG\@IBo6UE8EkU_X`Gn[2fKpqF7+6>3$NkId'F(hQO"Znp_7`V/cl(LKEoWU)*^)j&]>k*$'Nsm\/')NF6mpYe[Dn$3N)E_=^ocd[fu@:J"k`-."\N_6f;QA9RFPl^8u>semLY45e]NjIB.e@GHTB@A;\@IkgA4C@5+O#reX<Idk%IrMfs0h(923+/"%a:P:\66Wl%,NY"ejf-0%VG,rI10FQWZ,A4cZ_/AhE=D71s;DO'J3Y8R%\T#Tr%s2_tOH?+AF+/eq\G[Xue01B_i~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 757
>>
stream
Gatn#h/8]I&;BTO'M"98ftANj+1LNhj),c7A37#u'a_u+Y_d'JU^P[ET>%p1*'(!f\nXN5L=r!dc#O"/6ZMDV2r+IH'W:+TF9D.>\trQ&!=8(@lfjH\9hR8oa)^*a,L*Yj;rie>AiPN\g!EE2BT<L7,?#ZA[lmSehij`Q+^mY1P_:CfqI-d#^qR,V&PH?"2\1`A@1^D1!U*V3\!#]JS;0(oI?(!.^Jm'O<RKGqW>_=B&E1>Qnhau:=F!j8P[V2,O<u9Z9)MC%<$dtj6^BRNF](")@4Ru22587oH&8K5<tV^5?=-b?c_-q,cgBn9Z!f_TOV].?'ZopCin=tRgTK\LQL+?RgbGG\Ds!RH[Pg_+Y\]p#jP^"C(0uFF+q*6KfJoNL"<F*<7.+#OK1sfi3*RRL?V#^'9GX<tYMS``jjfuVA5!<lZbW2<iC)e1C0%RG=X?LVK%[L]\BF/)*9XY)e-j?N@M/Fb:Vm5DDco1^U3L"qN/Q\tG["aCVhsX4'cDK>OcX&:dgJi%aQ0\SP.=bcB5p-W^lL1?;+qclE0Pfu0.at@e[B#d(#?k,UWhQ%pIt,[;U-Q^lM=pb;`Wg`HT""2(g?@rNA*2\(*#Q4qU:+dh+'^j@4=Z3(fZFtCYo$CUUFh&8cR%3`k(KQB,T`gQ+.KkfGX-J/oK]uB9-Q:$b#*n$c9D%,X>\mZD+qT(g>n%)L$%$Z5qC@\Pl=qR>um)]`.sXBV[IdgTtcZ*-;[T4IsTCiBco[JQ%$d~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 735
>>
stream
GatUq?#SFN'Sc)P($Dae<12D7E8YW!g9+>_Yr1i"`\\QR#VJlB`<ipOISKs#TlpeT92Uppm.^&?CR]oc)f=H\']:HmrXB%R+IW?(L^2<j>XuKb4hAp\4lUd=(B=sN`:mNgp9S2E!ldAFL`8DCJ,pOto`IY2ZX_E(R4,HU$u"9aKXJVkLJ"!M(5iQVSfi>\.t4bj2-'&W/)\sXY:"RBS'W16DR%u?jpfld(:fUWGmWO&HSe!Yn\q^7,9,tl3)+X)OL9\Ke4q70)F6u'<9T`<"h@07-=G%7#Ee3J]`,+=DuV%2c#X*u8rpoH[*G="=:e/[<1!!]N7T0#!_%t902ZnCE#c_^rq:XA+kn=`7`bp`'FQIbe/;jPO\_'SV>edT"Q#9(NuA\a9RlU7<@)-WBVeW8JW7<n;G=;&Kbk_#nL]eR!8N!K\GdCtbb3i;%<p0RRl0L3_SRN`)P<PU>*Fb2$*i(#&:QI%q0<0*LUg0bp1jm;He,FJp#!-5+,8O';n)Fbc73>5[*D<YXb3Y&Y4fE46Z>Sa`ftPSeN-P5TAN`3]\V*K<A#W8+N2.@-X%3=1_fm-J$2*.K6:^CA8"Fo\<f?gQ86Is*8\)o7YbaSm$)7EKJ=uo.oN%VZE9IU8TpGKQfWT6SLM8;^uBCOq;<@WZ7"`\iDalX$=$2._,@<:ZPZ!%l/414i9h^-;Qoe')X^lNQ[]n@''rUV[D<]$V-d-Ep5a+Tr;gEmk@a~>endstream
endobj
xref
0 30
0000000000 65535 f
0000000061 00000 n
0000000133 00000 n
0000000240 00000 n
0000000352 00000 n
0000000467 00000 n
0000000672 00000 n
0000000777 00000 n
0000000982 00000 n
0000001187 00000 n
0000001392 00000 n
0000001598 00000 n
0000001804 00000 n
0000002010 00000 n
0000002216 00000 n
0000002294 00000 n
0000002500 00000 n
0000002706 00000 n
0000002776 00000 n
0000003057 00000 n
0000003178 00000 n
0000003636 00000 n
0000005172 00000 n
0000005844 00000 n
0000007100 00000 n
0000008606 00000 n
0000009854 00000 n
0000011338 00000 n
0000011746 00000 n
0000012594 00000 n
trailer
<<
/ID
[<c2b0ca93638236c118c39bf1e4748e63><c2b0ca93638236c118c39bf1e4748e63>]
% ReportLab generated PDF document -- digest (opensource)
/Info 18 0 R
/Root 17 0 R
/Size 30
>>
startxref
13420
%%EOF

View File

@@ -0,0 +1,622 @@
# Les 9 — Supabase Auth
## Live Coding Guide voor Docent
This is your cheat sheet for the full lesson. Follow the timing in Les09-Docenttekst.md.
---
## DEEL 1A: UITLEG AUTH (09:1010:00)
### 09:10 | SLIDE 4: Wat is Auth?
**Demo:** Open https://supabase.com/dashboard
Vertel:
"Authenticatie is: wie ben jij? Login en password, je identiteit bewijzen.
Autorisatie is: wat mag je doen? Wie mag polls maken? Dit regelen we met RLS policies.
Supabase Auth beheert alles: signUp, login, sessies, JWT tokens."
**Stap 1:** Dashboard openen, project selecteren
**Stap 2:** Klik Authentication → Providers → Email
**Stap 3:** Toon: "Disable Email Confirmations" is AAN
Vertel:
"Deze checkbox is cruciaal voor testen. Normaal zouden users een confirmation email krijgen voordat ze inloggen. Dat slaan we over voor deze les. In productie zet je dit uit."
---
### 09:20 | SLIDE 5: Auth Functies
**Vertel:**
"Supabase Auth heeft vier kern functies:"
**Code tonen (copy-paste in terminal of code editor):**
```typescript
// 1. signUp — Nieuw account
const { error } = await supabase.auth.signUp({
email: "user@example.com",
password: "secure123"
});
if (error) console.error(error.message);
// 2. signInWithPassword — Inloggen
const { error } = await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "secure123"
});
if (error) console.error(error.message);
// 3. signOut — Uitloggen
await supabase.auth.signOut();
// 4. getUser — Wie is ingelogd?
const { data: { user } } = await supabase.auth.getUser();
console.log(user.email); // "user@example.com"
console.log(user.id); // "abc-123-def"
```
Vertel:
"Diese vier functies zijn alles wat je nodig hebt. Error handling: altijd checken of `error` null is."
---
### 09:30 | SLIDE 6: Server vs Browser Client
**Vertel:**
"Supabase Auth werkt op twee plaatsen:
1. **Server (Node.js)** — Secure, via cookies
2. **Browser (React)** — Less secure, via localStorage
We gebruiken @supabase/ssr. Dit switcht automatisch."
**Show left code block (Server Client):**
```typescript
// lib/supabase-server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch { }
},
},
}
);
}
```
Vertel:
"Server client gebruikt cookies. Cookies kunnen beveiligd worden (httpOnly, secure-only). Dit is safer."
**Show right code block (Browser Client):**
```typescript
// lib/supabase-browser.ts
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
Vertel:
"Browser client werkt in React. localStorage is minder secure (scripts kunnen het uitlezen), maar nodig voor login forms."
**Key difference:**
"Server component (Navbar) → Server client (getUser)
Client component (LoginForm) → Browser client (signUp, signIn, signOut)"
---
## DEEL 1B: SAMEN CODEREN (10:0010:15)
Students volgen mee terwijl je dit live codeert (of ze kopieren uit les09-live-coding-guide.md).
### Stap 1: npm install
```bash
npm install @supabase/ssr
```
Wacht tot dit klaar is.
### Stap 2: lib/supabase-server.ts
Maak dit bestand aan:
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch { }
},
},
}
);
}
```
### Stap 3: lib/supabase-browser.ts
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
### Stap 4: middleware.ts (ROOT of project, naast app/)
```typescript
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return request.cookies.getAll(); },
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
await supabase.auth.getUser();
return supabaseResponse;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"],
};
```
Vertel:
"Middleware runt op elke request. `await supabase.auth.getUser()` refresht de session token. Dit zorgt dat je niet uitgelogd wordt als je token expired."
### Stap 5: app/auth/callback/route.ts
```typescript
import { NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase-server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
if (code) {
const supabase = await createSupabaseServerClient();
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(origin);
}
```
Vertel:
"Dit is voor OAuth (Google, GitHub). Supabase stuur je hier naartoe na login. De `code` wordt ge-exchanged voor een session. Voor nu: boilerplate, niet essentieel."
### Test middleware
Vertel:
"Test of middleware werkt: open http://localhost:3000. Je zou geen errors moeten zien. In browser dev tools → Application → Cookies → zoek naar `sb-*`. Die cookies beteken dat middleware goed werkt."
---
## PAUZE (10:1510:30)
---
## DEEL 2: ZELF DOEN (10:3011:30)
Students bouwen nu zelf. Jij loopt rond, helpt, en toont code op beamer als studenten stuck zijn.
### Zelf Doen Checklist (wat moet elke student doen):
- [ ] app/signup/page.tsx
- [ ] app/login/page.tsx
- [ ] components/LogoutButton.tsx
- [ ] components/Navbar.tsx
- [ ] app/layout.tsx updated
- [ ] Test signup → login → poll maken → logout
---
### 1. app/signup/page.tsx
Dit is een 'use client' component met form. Students schrijven dit zelf, maar hier is de reference:
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function SignUp() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const { error } = await supabase.auth.signUp({ email, password });
if (error) { setMessage(error.message); }
else { setMessage("Account aangemaakt!"); router.push("/login"); }
setLoading(false);
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Registreren</h1>
<form onSubmit={handleSignUp} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm font-medium mb-1">Wachtwoord</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded" minLength={6} required />
</div>
<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..." : "Registreren"}
</button>
</form>
{message && <p className="mt-4 text-sm text-center">{message}</p>}
<p className="mt-4 text-sm text-center">
Al een account? <Link href="/login" className="text-blue-600 hover:underline">Inloggen</Link>
</p>
</div>
);
}
```
**Students moeten begrijpen:**
- `'use client'` = React component
- `createSupabaseBrowserClient()` = browser auth
- `supabase.auth.signUp({ email, password })` = nieuwe user
- `if (error)` = error handling
- `router.push("/login")` = redirect na success
---
### 2. app/login/page.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
import { useState } from "react";
import Link from "next/link";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage("");
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) { setMessage(error.message); }
else { router.push("/"); router.refresh(); }
setLoading(false);
};
return (
<div className="w-full max-w-md mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Inloggen</h1>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<div>
<label className="block text-sm font-medium mb-1">Wachtwoord</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 border rounded" required />
</div>
<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..." : "Inloggen"}
</button>
</form>
{message && <p className="mt-4 text-sm text-red-600 text-center">{message}</p>}
<p className="mt-4 text-sm text-center">
Nog geen account? <Link href="/signup" className="text-blue-600 hover:underline">Registreren</Link>
</p>
</div>
);
}
```
**Key difference van signup:**
- `signInWithPassword()` i.p.v. `signUp()`
- `router.refresh()` om Navbar te update
- Error styling: `text-red-600`
---
### 3. components/LogoutButton.tsx
```typescript
'use client'
import { createSupabaseBrowserClient } from "@/lib/supabase-browser";
import { useRouter } from "next/navigation";
export function LogoutButton() {
const router = useRouter();
const supabase = createSupabaseBrowserClient();
const handleLogout = async () => {
await supabase.auth.signOut();
router.push("/");
router.refresh();
};
return (
<button onClick={handleLogout} className="text-sm text-gray-600 hover:text-gray-900">
Uitloggen
</button>
);
}
```
**Dit is een klein client component. Belangrijk:**
- `'use client'` (event handler)
- `signOut()` — geen params
- `router.refresh()` — update Navbar
---
### 4. components/Navbar.tsx
Dit is het interessantste component. Server Component met `async`:
```typescript
import Link from "next/link";
import { createSupabaseServerClient } from "@/lib/supabase-server";
import { LogoutButton } from "./LogoutButton";
export async function Navbar() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return (
<nav className="w-full border-b p-4 flex justify-between items-center">
<Link href="/" className="text-xl font-bold">QuickPoll</Link>
<div className="flex items-center gap-4">
{user ? (
<>
<span className="text-sm text-gray-600">{user.email}</span>
<LogoutButton />
</>
) : (
<>
<Link href="/login" className="text-sm hover:underline">Inloggen</Link>
<Link href="/signup" className="text-sm bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700">Registreren</Link>
</>
)}
</div>
</nav>
);
}
```
**Vertel:**
"Navbar is een **Server Component** (geen 'use client'). Dit betekent `async` is ok. We callen `getUser()` direct — geen hooks nodig!
`getUser()` gebruikt server client + cookies. Dit is beveiligd en efficient."
**Logica:**
- Als `user` bestaat: toon email + LogoutButton
- Anders: toon Inloggen + Registreren links
---
### 5. app/layout.tsx (update)
Voeg Navbar toe:
```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { Navbar } from "@/components/Navbar";
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
export const metadata: Metadata = { title: "QuickPoll", description: "Stem op je favoriete opties" };
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="nl">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Navbar />
{children}
</body>
</html>
);
}
```
---
### TROUBLESHOOTING (10:3011:30)
Als studenten stuck zijn, gebruik deze tabel:
| Symptoom | Oorzaak | Oplossing |
|----------|---------|----------|
| "Module not found: @supabase/ssr" | npm install niet gedaan | `npm install @supabase/ssr` |
| Navbar toont altijd "Inloggen" (ook na login) | getUser() returns null | Check: cookies middleware working? Browser dev tools → Cookies (zoek sb-*) |
| Login werkt, maar redirect loopt vast | router.refresh() niet in handleLogin | Voeg `router.refresh()` toe na success |
| "Invalid PKCE flow" | Browser client not configured | Check .env: NEXT_PUBLIC_SUPABASE_URL en ANON_KEY kloppen |
| Logout knop werkt niet | signOut() niet awaited | Zorg: `await supabase.auth.signOut()` |
| Navbar.tsx error: "cannot use async in component" | Navbar is client component | Zorg: geen `'use client'` aan top van Navbar! |
---
### 11:00 | CHECK-IN: NAVBAR DEMO
Toon op beamer je eigen Navbar. Vertel:
"Navbar is een **Server Component**. Dit is uniek voor Next.js. In React kan je geen `async` functions gebruiken als components.
Hier kan het wel, omdat Next.js bij build-time Server Components render. Cookies zijn secure. getUser() is beveiligd. Dit is beter dan client-side auth check."
Toon: `const { data: { user } } = await supabase.auth.getUser();`
"Dat een lijn doet alles: leest cookies → vraagt Supabase → geeft user object."
---
### 11:15 | RLS UPDATE
Voer in Supabase dashboard SQL uit:
**SQL Editor → New Query:**
```sql
-- Enable RLS on polls table
ALTER TABLE polls ENABLE ROW LEVEL SECURITY;
-- Anyone can read polls
CREATE POLICY "polls_select_all" ON polls
FOR SELECT USING (true);
-- Only authenticated users can create polls
CREATE POLICY "polls_insert_authenticated" ON polls
FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);
-- Only the creator can update their own poll
CREATE POLICY "polls_update_owner" ON polls
FOR UPDATE USING (auth.uid() = created_by);
```
Vertel:
"RLS = Row Level Security. Dit enforces wie wat kan doen op database level.
- Iedereen (anoniem) kan polls zien (SELECT)
- Alleen ingelogde users (auth.uid() NOT NULL) mogen polls maken (INSERT)
- Alleen de maker mag hun eigen poll updaten (UPDATE)
auth.uid() is de user ID van Supabase. NULL als je niet ingelogd bent."
**Test:**
1. Open http://localhost:3000/create
2. Niet ingelogd → knop grijs / form gedeactiveerd
3. Inloggen
4. Poll aanmaken → werkt!
5. Uitloggen
6. /create opnieuw → weer grijs (RLS blokkeert INSERT)
---
### 11:3011:45 | VRAGEN & DEBUGGING
Loopround. Antwoord vragen:
**Q: Hoe debug ik auth?**
A: Supabase dashboard → Auth → Users. Daar zie je alle users. Of: Browser dev tools → Application → Cookies (zoek `sb-*` prefix).
**Q: Hoe reset ik mijn test account?**
A: Dashboard → Auth → Users → klik user → delete → registreer opnieuw.
**Q: Waarom zie ik geen email na login?**
A: Middleware werkt niet. Zorg middleware.ts in root project staat. Check: `matcher` is correct.
**Q: Kan ik multiple providers (Google, GitHub) toevoegen?**
A: Ja, later. Dashboard → Auth → Providers. Voor nu: email-password is genoeg.
---
## HUISWERK & AFSLUITING (11:4512:00)
**Huiswerk (slides 9):**
1. **Profiel pagina (Les 10)**
```typescript
// app/profile/page.tsx
import { createSupabaseServerClient } from "@/lib/supabase-server";
export default async function ProfilePage() {
const supabase = await createSupabaseServerClient();
const { data: { user } } = await supabase.auth.getUser();
return <div>{user?.email}</div>;
}
```
2. **Maker tonen bij poll (Les 10)**
- Voeg `created_by uuid` kolom toe polls tabel
- Update INSERT in /create om `created_by: user.id` toe te voegen
- Toon "Gemaakt door: [email]" op homepage
3. **Google OAuth (Bonus)**
- Supabase dashboard → Auth → Providers → Google
- Copy Client ID en Secret van Google Cloud
- Voeg button toe: `signInWithOAuth({ provider: 'google' })`
**Afsluiting (slide 10):**
"Volgende les: **Deployment**. We zetten je app live op Vercel. Dan kunnen je vrienden echt je polls gebruiken!
Daarna: Google OAuth, profiel updaten, meer security features.
Vandaag hebben we de kern van auth gebouwd. Goed gedaan!"
---
## DOCS
Supabase Auth docs: https://supabase.com/docs/guides/auth/server-side/nextjs
Next.js Server Components: https://nextjs.org/docs/app/building-your-application/rendering/server-components

View File

@@ -0,0 +1,270 @@
# Les 9 — Supabase Auth
## Slide Overzicht
---
## Slide 1: Title
### Les 9 — Supabase Auth
**Visual:** Large centered title with QuickPoll icon
- Background: CREAM
- "Les 9" in BLUE
- "Supabase Auth" in BLACK
- Subtitle: "signUp, signIn, signOut, Navbar, RLS"
---
## Slide 2: Terugblik (Recap)
### Waar staan we?
**Content:**
- Supabase project aangemaakt en gekoppeld
- /create pagina gebouwd
- Server Component + VoteForm patroon
- Polls werkend in database
- Real-time votes
- "Nu: beveiligde login"
**Visual:**
- Left: screenshot van huidige app
- Right: checkmarks of badges
---
## Slide 3: Planning
### Vandaag — 120 minuten
| Tijd | Onderwerp | Duur |
|------|-----------|------|
| 09:0009:10 | Welkom + Terugblik | 10 min |
| 09:1010:00 | Uitleg Auth | 50 min |
| 10:0010:15 | Samen Middleware bouwen | 15 min |
| 10:1510:30 | **Pauze** | 15 min |
| 10:3011:30 | Zelf Doen (signup, login, Navbar) | 60 min |
| 11:3011:45 | Vragen & Debugging | 15 min |
| 11:4512:00 | Huiswerk + Afsluiting | 15 min |
**Visual:** Timeline with YELLOW background, icons per blok
---
## Slide 4: Wat is Auth?
### Authenticatie vs Autorisatie
**Authenticatie (WHO):**
- Wie ben jij?
- Email + password
- Supabase verifies en geeft JWT token
- User object: email, id, created_at
**Autorisatie (WHAT):**
- Wat mag je doen?
- Wie mag polls maken?
- Later: RLS policies
**Features van Supabase Auth:**
- Email/password signup & signin
- Session management (cookies)
- JWT tokens
- Password reset
- Multi-factor auth (later)
- OAuth (Google, GitHub, etc.)
**Visual:**
- Left: "Authentication" icon (person + key)
- Right: "Authorization" icon (person + checkmark)
- Supabase logo
---
## Slide 5: Auth Functies
### Vier Core Operations
**signUp**
```typescript
const { error } = await supabase.auth.signUp({
email: "user@example.com",
password: "secure123"
});
```
→ Account aanmaken
**signInWithPassword**
```typescript
const { error } = await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "secure123"
});
```
→ Inloggen
**signOut**
```typescript
await supabase.auth.signOut();
```
→ Uitloggen
**getUser**
```typescript
const { data: { user } } = await supabase.auth.getUser();
// user.email, user.id, user.email_confirmed_at
```
→ Huidige user
**Visual:** Code blocks in BLUE boxes, icons above each
---
## Slide 6: Server vs Browser Client
### Two Clients, One Auth
**Server Client** (@supabase/ssr)
```typescript
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) { /* ... */ },
},
}
);
}
```
**Use in:**
- Middleware (refresh token)
- Server Components (Navbar, getUser)
- API routes
**Browser Client** (@supabase/ssr)
```typescript
import { createBrowserClient } from "@supabase/ssr";
export function createSupabaseBrowserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
```
**Use in:**
- Client Components ('use client')
- Login forms
- Logout buttons
**Visual:** Two side-by-side code blocks
- Left: Server (BLUE bg), lock icon
- Right: Browser (PINK bg), web icon
**Key difference:**
- Server: Cookies (secure, secure-only, httpOnly)
- Browser: localStorage (accessible, but less safe)
---
## Slide 7: Pauze
### Pauze!
**Visual:** Relaxed illustration, "15 minuten", clock
---
## Slide 8: Zelf Doen — Auth Bouwen
### Nu jij — 60 minuten
**To-Do:**
- [ ] app/signup/page.tsx (form)
- [ ] app/login/page.tsx (form)
- [ ] components/LogoutButton.tsx
- [ ] components/Navbar.tsx (Server Component + getUser)
- [ ] app/layout.tsx (add `<Navbar />`)
- [ ] Update RLS policies (authenticated only!)
**Reference code beschikbaar** (docent toont op beamer)
**Process:**
1. Start simpel: form met email + password inputs
2. Voeg supabase.auth.signUp / signInWithPassword toe
3. Test in browser
4. Navbar: toon email of login link
5. RLS: polls INSERT nur voor authenticated users
**Expected result:**
- Registreren → inloggen → poll maken → uitloggen
- Na logout: kan geen poll meer maken (RLS!)
**Visual:** Big BLUE background, "Bouw Auth" header, checklist
---
## Slide 9: Huiswerk
### Volgende Stap
**Verplicht (Les 10):**
1. Profiel pagina (app/profile/page.tsx)
- Toon user.email, user.id
- Later: password update form
2. Maker tonen bij poll
- Voeg `created_by` kolom toe polls tabel
- Toon "Gemaakt door: [email]" bij elke poll
- RLS: alleen maker mag aanpassen (UPDATE)
**Bonus (optioneel):**
1. Google OAuth signup
- Supabase dashboard → Auth → Providers → Google
- Voeg "Sign in with Google" knop toe
2. Password reset
- Email link naar reset form
- supabase.auth.resetPasswordForEmail()
**Visual:** Checklist, bonus items in PINK
---
## Slide 10: Afsluiting
### Volgende Les — Deployment
**Wat hebben we gedaan vandaag:**
- Auth concepten: authenticatie vs autorisatie
- Supabase Auth functies: signUp, signIn, signOut, getUser
- Server vs browser client
- Middleware voor session refresh
- Navbar met authenticated user
- RLS policies
**Volgende keer:**
- Vercel deployment
- Google OAuth
- Profiel pagina
- Meer security!
**Vragen? Feedback?**
**Visual:** Vercel logo, rocket icon, "Deployment!" in YELLOW
---
## Slide Summary
| # | Title | Duration | Key Content |
|---|-------|----------|-------------|
| 1 | Title | Opening | Les 9 — Supabase Auth |
| 2 | Recap | 09:10 | Where we are |
| 3 | Plan | 09:05 | 120-min schedule |
| 4 | Auth Concepts | 09:10 | Auth vs AuthN, Supabase features |
| 5 | Functions | 09:20 | signUp, signIn, signOut, getUser |
| 6 | Clients | 09:30 | Server vs Browser (@supabase/ssr) |
| 7 | Break | 10:15 | 15 min pauze |
| 8 | Build | 10:30 | Students implement auth |
| 9 | Homework | 11:45 | Profile, maker, Google OAuth |
| 10 | Closing | 11:55 | Next: Deployment |

Binary file not shown.