Compare commits
3 Commits
ca11a67016
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f65c24ffcd | |||
| 426b9f89d9 | |||
| b9ffee586f |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
207
Les08-Docenttekst.md
Normal file
207
Les08-Docenttekst.md
Normal 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:00–09: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:15–09: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:45–10: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:15–10:30 | PAUZE (15 min)
|
||||||
|
📌 Slide 7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10:30–10: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:45–11: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:30–11: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:45–12: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
346
Les08-Lesopdracht.pdf
Normal 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=EEs9hJt.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
186
Les08-Slide-Overzicht.md
Normal 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
BIN
Les08-Slides.pptx
Normal file
Binary file not shown.
575
Les08-Supabase+Nextjs/Les08-Docenttekst.md
Normal file
575
Les08-Supabase+Nextjs/Les08-Docenttekst.md
Normal 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:00–09: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:10–10: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:15–10:30 | PAUZE (15 min)
|
||||||
|
📌 Slide 7
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10:30–11: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:30–11: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:45–12: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!
|
||||||
238
Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf
Normal file
238
Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf
Normal 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
|
||||||
551
Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md
Normal file
551
Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md
Normal 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:10–10: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:30–11: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:00–09:10:** Welkom + Slide 1, 2, 3
|
||||||
|
- **09:10–10:15:** Live Coding (Stap 1–11) + Slide 6 halverwege
|
||||||
|
- **10:15–10:30:** Pauze (Slide 7)
|
||||||
|
- **10:30–11:30:** Zelf Doen + Theorie (Slide 8)
|
||||||
|
- **11:30–11:45:** Vragen
|
||||||
|
- **11:45–12:00:** Huiswerk + Afsluiting (Slide 9, 10)
|
||||||
169
Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md
Normal file
169
Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md
Normal 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
|
||||||
BIN
Les08-Supabase+Nextjs/Les08-Slides.pptx
Normal file
BIN
Les08-Supabase+Nextjs/Les08-Slides.pptx
Normal file
Binary file not shown.
@@ -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:00–09: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:10–10: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:10–09: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:15–09: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:25–09: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:35–09: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:45–10: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:00–10: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:10–10: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:15–10: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:30–11: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:00–11: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:30–11: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:45–12: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**
|
|
||||||
@@ -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
@@ -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!"
|
|
||||||
Binary file not shown.
552
Les09-Supabase-Auth/Les09-Docenttekst.md
Normal file
552
Les09-Supabase-Auth/Les09-Docenttekst.md
Normal 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:00–09: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:10–10:00: Uitleg Auth concepten + Demo
|
||||||
|
- 10:00–10:15: Samen Middleware + Auth Callback bouwen
|
||||||
|
- 10:15–10:30: Pauze
|
||||||
|
- 10:30–11:30: Zelf Doen (signup, login, logout, Navbar)
|
||||||
|
- 11:30–11:45: Vragen
|
||||||
|
- 11:45–12:00: Huiswerk + Afsluiting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 09:10–10: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:00–10: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:15–10:30 | Pauze
|
||||||
|
**Slide 7**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10:30–11: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:30–11: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:45–12: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
|
||||||
263
Les09-Supabase-Auth/Les09-Lesopdracht.pdf
Normal file
263
Les09-Supabase-Auth/Les09-Lesopdracht.pdf
Normal 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
|
||||||
622
Les09-Supabase-Auth/Les09-Live-Coding-Guide.md
Normal file
622
Les09-Supabase-Auth/Les09-Live-Coding-Guide.md
Normal 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:10–10: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:00–10: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:15–10:30)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DEEL 2: ZELF DOEN (10:30–11: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:30–11: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:30–11: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:45–12: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
|
||||||
270
Les09-Supabase-Auth/Les09-Slide-Overzicht.md
Normal file
270
Les09-Supabase-Auth/Les09-Slide-Overzicht.md
Normal 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:00–09:10 | Welkom + Terugblik | 10 min |
|
||||||
|
| 09:10–10:00 | Uitleg Auth | 50 min |
|
||||||
|
| 10:00–10:15 | Samen Middleware bouwen | 15 min |
|
||||||
|
| 10:15–10:30 | **Pauze** | 15 min |
|
||||||
|
| 10:30–11:30 | Zelf Doen (signup, login, Navbar) | 60 min |
|
||||||
|
| 11:30–11:45 | Vragen & Debugging | 15 min |
|
||||||
|
| 11:45–12: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 |
|
||||||
BIN
Les09-Supabase-Auth/Les09-Slides.pptx
Normal file
BIN
Les09-Supabase-Auth/Les09-Slides.pptx
Normal file
Binary file not shown.
Reference in New Issue
Block a user