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