fix: add 9

This commit is contained in:
2026-03-31 16:34:28 +02:00
parent b9ffee586f
commit 426b9f89d9
24 changed files with 1533 additions and 3758 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.