# Les 7 — Live Coding Guide
## Van In-Memory naar Supabase
> **Jouw spiekbriefje.** Dit bestand staat op je privéscherm. Op de beamer draait Cursor.
> Volg stap voor stap. Typ exact wat hier staat. Leg uit met de "Vertel:" blokken.
---
## Planning
| Blok | Tijd | Onderwerp |
|------|------|-----------|
| Deel 1 | 09:00 – 09:30 | Poll-app afmaken (stemmen werkend) |
| Deel 2 | 09:30 – 10:15 | Supabase introductie — No Code |
| Pauze | 10:15 – 10:30 | — |
| Deel 3 | 10:30 – 11:30 | Supabase koppelen aan Next.js |
| Afsluiting | 11:30 – 12:00 | Testen, Q&A, huiswerk |
---
# DEEL 1: Poll-app afmaken (09:00 – 09:30)
> **Vertel:** "Vorige les hebben we de QuickPoll app gebouwd, maar het stemmen werkt nog niet echt. De POST route logt alleen 'hello from server'. Vandaag maken we dat eerst werkend, en daarna koppelen we alles aan een echte database: Supabase."
---
## Stap 1.1 — `votePoll()` functie toevoegen aan `data.ts`
> **Vertel:** "We hebben `getPolls()` en `getPollById()` maar nog geen functie om een stem te verwerken. Die voegen we nu toe."
Open `lib/data.ts` en voeg onderaan toe:
```typescript
export function votePoll(id: string, optionIndex: number): Poll | undefined {
const poll = polls.find((p) => p.id === id);
if (!poll) return undefined;
if (optionIndex < 0 || optionIndex >= poll.options.length) return undefined;
poll.votes[optionIndex]++;
return poll;
}
```
> **Vertel:** "Simpel: we zoeken de poll, checken of de index geldig is, en verhogen de votes. We returnen de poll zodat de API de updated data kan terugsturen."
---
## Stap 1.2 — POST route werkend maken
> **Vertel:** "Nu gaan we de POST route fixen. Die deed nog niks — alleen console.log. We moeten de body uitlezen, votePoll aanroepen, en een response terugsturen."
Open `app/api/polls/[id]/route.ts` en vervang de hele inhoud:
```typescript
import { NextResponse } from "next/server";
import { votePoll } from "@/lib/data";
interface RouteParams {
params: Promise<{ id: string }>;
}
export async function POST(request: Request, { params }: RouteParams) {
const { id } = await params;
const body = await request.json();
const optionIndex = body.optionIndex;
const updatedPoll = votePoll(id, optionIndex);
if (!updatedPoll) {
return NextResponse.json(
{ error: "Poll niet gevonden of ongeldige optie" },
{ status: 400 }
);
}
return NextResponse.json(updatedPoll);
}
```
> **Vertel:** "We lezen de body uit met `request.json()`, halen de `optionIndex` eruit, en roepen onze nieuwe `votePoll` functie aan. Als het mislukt sturen we een 400 error terug, anders de updated poll."
---
## Stap 1.3 — Server Component + VoteForm split
> **Vertel:** "Nu gaan we de detail pagina goed opzetten. Op dit moment is het hele bestand een Client Component met `'use client'`. Maar dat is niet hoe Next.js bedoeld is. De pagina zelf moet een Server Component zijn — die haalt de data op. En alleen het stukje dat interactief is (stemmen), dat wordt een apart Client Component. Dit is het belangrijkste patroon in Next.js."
### Stap 1.3a — VoteForm component aanmaken
Maak een nieuw bestand `components/VoteForm.tsx`:
```typescript
'use client'
import { Poll } from "@/types";
import { PollItem } from "./PollItem";
import { useState } from "react";
export function VoteForm({ poll: initialPoll }: { poll: Poll }) {
const [poll, setPoll] = useState(initialPoll);
const onVote = async (option: string) => {
// Zoek de INDEX van de option in de array
const optionIndex = poll.options.indexOf(option);
const response = await fetch(`/api/polls/${poll.id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ optionIndex }),
});
if (response.ok) {
const updatedPoll = await response.json();
setPoll(updatedPoll);
}
};
return ;
}
```
> **Vertel:** "Dit is het Client Component — het enige stuk dat `'use client'` nodig heeft. Het ontvangt de poll als prop, slaat die op in state, en handelt het stemmen af. Na het stemmen updaten we de state met de response van de API."
### Stap 1.3b — Page als Server Component
Open `app/poll/[id]/page.tsx` en vervang de **hele** inhoud:
```typescript
import { getPollById } from "@/lib/data";
import { VoteForm } from "@/components/VoteForm";
import { notFound } from "next/navigation";
interface PageProps {
params: Promise<{ id: string }>;
}
export default async function PollPage({ params }: PageProps) {
const { id } = await params;
const poll = getPollById(id);
if (!poll) notFound();
return (
{poll.question}
);
}
```
> **Vertel:** "Kijk wat hier gebeurt: de pagina is een Server Component. Geen `'use client'`, geen `useState`, geen `useEffect`. De data wordt direct opgehaald met `getPollById()` — op de server. Dan geven we de poll als prop door aan de VoteForm. Dat is het patroon: **Server Component haalt data, Client Component doet interactie.**"
>
> **Vertel:** "En we gebruiken `notFound()` — als de poll niet bestaat, toont Next.js automatisch een 404 pagina. Dat hadden we vorige les geleerd."
---
## Stap 1.4 — GET route toevoegen
> **Vertel:** "We hebben een POST route, maar nog geen GET. Die hebben we nodig voor de VoteForm — na het stemmen fetcht die de updated poll via de API."
Open `app/api/polls/[id]/route.ts` en vervang met de complete versie met GET + POST:
```typescript
import { NextResponse } from "next/server";
import { getPollById, votePoll } from "@/lib/data";
interface RouteParams {
params: Promise<{ id: string }>;
}
export async function GET(request: Request, { params }: RouteParams) {
const { id } = await params;
const poll = getPollById(id);
if (!poll) {
return NextResponse.json({ error: "Poll niet gevonden" }, { status: 404 });
}
return NextResponse.json(poll);
}
export async function POST(request: Request, { params }: RouteParams) {
const { id } = await params;
const body = await request.json();
const optionIndex = body.optionIndex;
const updatedPoll = votePoll(id, optionIndex);
if (!updatedPoll) {
return NextResponse.json(
{ error: "Poll niet gevonden of ongeldige optie" },
{ status: 400 }
);
}
return NextResponse.json(updatedPoll);
}
```
---
## Stap 1.5 — Visuele feedback toevoegen aan PollItem
> **Vertel:** "Laten we het iets mooier maken. We tonen nu het aantal votes en een simpele progress bar per optie."
Open `components/PollItem.tsx` en vervang de inhoud:
```typescript
'use client'
import { Poll } from "@/types"
type PollItemProps = {
poll: Poll,
onOptionClick?: (option: string) => void
}
type PollItemOptionProps = {
option: string
votes: number
percentage: number
onClick?: (option: string) => void
}
export const PollItemOption = ({ option, votes, percentage, onClick }: PollItemOptionProps) => {
return (
onClick?.(option)}
className="relative my-2 p-3 border rounded cursor-pointer hover:bg-gray-50 overflow-hidden"
>
{/* Achtergrond bar */}
{/* Tekst bovenop de bar */}
{poll.options.map((option, index) => {
const votes = poll.votes[index];
const percentage = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
return (
)
})}
)
}
```
> **Vertel:** "Nu tonen we bij elke optie het aantal stemmen, een percentage, en een visuele balk. De balk groeit mee met het percentage. We gebruiken `transition-all` zodat het soepel animeert."
---
## ✅ Check: Deel 1
> **Check:** Start de dev server (`npm run dev`), ga naar `http://localhost:3000`, klik op een poll, stem op een optie. Je ziet nu:
> - De stem wordt verwerkt
> - De votes updaten direct in de UI
> - De percentage bars verschijnen
>
> **Vertel:** "Dit werkt nu, maar er is een probleem: als je de pagina refresht, zijn je stemmen nog steeds bewaard — maar als je de server herstart, is alles weg. Dat is omdat onze data in het geheugen staat. Vandaag gaan we dat oplossen met een echte database."
---
# DEEL 2: Introductie Supabase — No Code (09:30 – 10:15)
> **Vertel:** "Nu gaan we het over een echte database hebben. We gaan Supabase gebruiken. Eerst zonder code — puur via de website."
---
## Stap 2.1 — Wat is Supabase?
> **Vertel:** "Supabase is een open-source alternatief voor Firebase. Het geeft je een hele backend out of the box:"
>
> - **PostgreSQL database** — een echte, professionele SQL database
> - **Authenticatie** — login, registratie, OAuth (Google, GitHub, etc.)
> - **Storage** — bestanden uploaden (afbeeldingen, PDF's, etc.)
> - **Realtime** — live updates als data verandert
> - **Edge Functions** — serverless functies
>
> **Vertel:** "Het grote verschil met Firebase: Supabase gebruikt PostgreSQL. Dat is een open-source database die al 30+ jaar bestaat. Als je Supabase leert, leer je ook SQL — en dat is een skill die je overal kunt gebruiken."
>
> **Vertel:** "En het mooie: er is een gratis tier waarmee je prima kunt werken voor kleine projecten en leren."
---
## Stap 2.2 — Supabase project aanmaken
> **Vertel:** "We gaan nu samen een project aanmaken. Open je browser en ga naar supabase.com."
### Stappen op het scherm:
1. Ga naar **supabase.com** → klik **Start your project** (of **Sign In** als je al een account hebt)
2. Log in met **GitHub** (makkelijkst)
3. Klik **New Project**
4. Vul in:
- **Organization**: kies je org (of maak er een aan)
- **Project name**: `quickpoll`
- **Database Password**: genereer een sterk wachtwoord (sla het op!)
- **Region**: `West EU (Frankfurt)` — dichtste bij Nederland
5. Klik **Create new project**
6. Wacht ~30 seconden tot het project klaar is
> **Vertel:** "Je ziet nu het dashboard. Hier vind je alles: je database, je API keys, de Table Editor, SQL Editor. Laten we beginnen met tabellen maken."
---
## Stap 2.3 — `polls` tabel aanmaken via Table Editor
> **Vertel:** "We gaan nu onze database structuur opzetten. In onze code hadden we één array met Poll objecten. In een database splitsen we dat op in twee tabellen: polls en options. Dat heet normalisatie — elke tabel heeft zijn eigen verantwoordelijkheid."
### Stappen op het scherm:
1. Klik **Table Editor** in het linkermenu
2. Klik **Create a new table**
3. Vul in:
- **Name**: `polls`
- **Enable Row Level Security (RLS)**: laat aan staan (maar we gaan er zo een policy voor maken)
4. Kolommen:
- `id` → staat er al (uuid, primary key) ✅
- `created_at` → staat er al (timestamptz, default `now()`) ✅
- Klik **Add column**:
- **Name**: `question`
- **Type**: `text`
- Vink **Is Nullable** UIT (niet null)
5. Klik **Save**
> **Vertel:** "We hebben nu een polls tabel met drie kolommen: een automatisch gegenereerd id (uuid — dat is een uniek ID), een question (tekst), en created_at (wanneer de poll is aangemaakt). Supabase maakt id en created_at automatisch voor je."
---
## Stap 2.4 — `options` tabel aanmaken
> **Vertel:** "Nu de options tabel. Elke poll heeft meerdere opties. In plaats van een array in één kolom, maken we een aparte tabel met een verwijzing (foreign key) naar de poll."
### Stappen op het scherm:
1. Klik **New Table**
2. Vul in:
- **Name**: `options`
- **Enable RLS**: aan
3. Kolommen (naast id en created_at):
- Klik **Add column**:
- **Name**: `poll_id`
- **Type**: `uuid`
- **Is Nullable**: UIT
- Klik **Add column**:
- **Name**: `text`
- **Type**: `text`
- **Is Nullable**: UIT
- Klik **Add column**:
- **Name**: `votes`
- **Type**: `int8`
- **Default value**: `0`
4. Nu de foreign key: klik op het **link-icoontje** naast `poll_id`
- **Foreign table**: `polls`
- **Foreign column**: `id`
- **On delete**: `CASCADE` (als een poll wordt verwijderd, verdwijnen de opties ook)
5. Klik **Save**
> **Vertel:** "CASCADE betekent: als je een poll verwijdert, worden automatisch alle opties van die poll ook verwijderd. Dat voorkomt 'wees-data' — opties die naar een poll verwijzen die niet meer bestaat."
---
## Stap 2.5 — RLS policies instellen
> **Vertel:** "Supabase heeft Row Level Security — dat betekent dat standaard NIEMAND je data kan lezen of schrijven via de API. We moeten expliciet toestemming geven. Voor onze app willen we dat iedereen polls kan lezen en stemmen."
### Stappen op het scherm:
1. Ga naar **Authentication** → **Policies** (of via Table Editor → polls → **RLS Policies**)
2. Bij de `polls` tabel, klik **New Policy**
- **Policy name**: `Allow public read`
- **Allowed operation**: `SELECT`
- **Target roles**: `anon`
- **USING expression**: `true`
- Klik **Save**
3. Bij de `options` tabel, maak twee policies:
- **Policy 1 — Lezen**:
- **Name**: `Allow public read`
- **Operation**: `SELECT`
- **Target roles**: `anon`
- **USING**: `true`
- **Policy 2 — Updaten (stemmen)**:
- **Name**: `Allow public vote`
- **Operation**: `UPDATE`
- **Target roles**: `anon`
- **USING**: `true`
- **WITH CHECK**: `true`
> **Vertel:** "We geven de `anon` rol (dat zijn niet-ingelogde gebruikers) toestemming om polls en opties te lezen, en om opties te updaten (voor het stemmen). In een productie-app zou je dit veel strakker instellen, maar voor ons leerproject is dit prima."
---
## Stap 2.6 — Testdata toevoegen
> **Vertel:** "Laten we onze twee polls toevoegen, dezelfde als in onze code."
### In de Table Editor → `polls`:
1. Klik **Insert row**
2. Vul in: `question`: `Ik ben een vraag` (id en created_at worden automatisch ingevuld)
3. Klik **Save**
4. Voeg nog een rij toe: `question`: `Ik ben een vraag 2`
5. **Kopieer de id's** van beide polls — die heb je nodig voor de options!
### In de Table Editor → `options`:
1. Voeg rijen toe voor poll 1 (gebruik het id van de eerste poll als `poll_id`):
- `poll_id`: *[id van poll 1]*, `text`: `optie 1`, `votes`: `1`
- `poll_id`: *[id van poll 1]*, `text`: `optie 2`, `votes`: `1`
- `poll_id`: *[id van poll 1]*, `text`: `optie 3`, `votes`: `1`
- `poll_id`: *[id van poll 1]*, `text`: `optie 4`, `votes`: `1`
2. Doe hetzelfde voor poll 2
> **Check:** Je hebt nu 2 rijen in `polls` en 8 rijen in `options`.
---
## Stap 2.7 — SQL Editor verkennen
> **Vertel:** "Supabase heeft ook een SQL Editor. Hier kun je direct SQL queries uitvoeren. Laten we eens kijken wat Supabase onder water doet."
### In de SQL Editor, typ en run:
```sql
SELECT * FROM polls;
```
> **Vertel:** "Dit haalt alle polls op. Simpel toch? SQL is de taal waarmee je met databases praat."
```sql
SELECT * FROM options WHERE poll_id = '[plak hier een poll id]';
```
> **Vertel:** "Dit haalt alleen de opties op van één specifieke poll. De WHERE clausule filtert."
```sql
SELECT polls.question, options.text, options.votes
FROM polls
JOIN options ON options.poll_id = polls.id;
```
> **Vertel:** "Dit is een JOIN — we combineren data uit twee tabellen. Je ziet de vraag samen met elke optie en het aantal stemmen. Dit is de kracht van een relationele database."
---
## ☕ PAUZE (10:15 – 10:30)
> **Vertel:** "Na de pauze gaan we Supabase koppelen aan ons Next.js project. Dan halen we de data niet meer uit een array, maar uit de echte database."
---
# DEEL 3: Supabase koppelen aan Next.js (10:30 – 11:30)
---
## Stap 3.1 — Supabase client library installeren
> **Vertel:** "We gaan nu onze Next.js app koppelen aan Supabase. Eerst installeren we de Supabase JavaScript library."
In de terminal:
```bash
npm install @supabase/supabase-js
```
---
## Stap 3.2 — Environment variables instellen
> **Vertel:** "We hebben twee dingen nodig van Supabase: de project URL en de anon key. Die vind je in je dashboard onder Settings → API."
### Stappen op het scherm:
1. Ga naar **Settings** → **API** in het Supabase dashboard
2. Kopieer de **Project URL** en de **anon public key**
Maak een nieuw bestand `.env.local` in de root van je project:
```bash
NEXT_PUBLIC_SUPABASE_URL=https://jouw-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
> **Vertel:** "We gebruiken `NEXT_PUBLIC_` als prefix zodat deze variabelen ook in de browser beschikbaar zijn. Normaal gesproken wil je secrets NIET public maken, maar de anon key is ontworpen om veilig in de browser te gebruiken — de beveiliging zit in de RLS policies die we net hebben ingesteld."
>
> **Belangrijk:** "Voeg `.env.local` toe aan je `.gitignore` als die er nog niet in staat! Dit bestand mag NOOIT naar GitHub."
---
## Stap 3.3 — Supabase client aanmaken
> **Vertel:** "We maken een utility bestand aan dat de Supabase client configureert. Dit gebruiken we overal in onze app."
Maak een nieuw bestand `lib/supabase.ts`:
```typescript
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
```
> **Vertel:** "Drie regels code. We importeren `createClient` van Supabase, lezen de URL en key uit de environment variables, en exporteren een client instance. Het uitroepteken (`!`) zegt tegen TypeScript: 'ik weet zeker dat deze waarde bestaat'. We gebruiken dit ene `supabase` object overal in onze app."
---
## Stap 3.4 — Types updaten
> **Vertel:** "Onze database structuur is anders dan wat we in de code hadden. In de database hebben we aparte tabellen voor polls en options. Laten we onze types updaten."
Open `types/index.ts` en vervang de inhoud:
```typescript
export interface Poll {
id: string;
question: string;
created_at: string;
options: Option[];
}
export interface Option {
id: string;
poll_id: string;
text: string;
votes: number;
}
```
> **Vertel:** "We hebben nu twee types: Poll en Option. Een Poll heeft een array van Options. Dit matcht precies met onze twee database tabellen. Let op: `id` is nu een `string` (uuid) in plaats van een simpel nummer."
---
## Stap 3.5 — `data.ts` aanpassen voor Supabase
> **Vertel:** "Nu het belangrijkste: we vervangen onze hardcoded array door echte database queries. We herschrijven `lib/data.ts` volledig."
Open `lib/data.ts` en vervang de **hele** inhoud:
```typescript
import { supabase } from "./supabase";
import { Poll, Option } from "@/types";
export async function getPolls(): Promise {
const { data: polls, error } = await supabase
.from("polls")
.select("*, options(*)")
.order("created_at", { ascending: false });
if (error) {
console.error("Error fetching polls:", error);
return [];
}
return polls || [];
}
export async function getPollById(id: string): Promise {
const { data: poll, error } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", id)
.single();
if (error) {
console.error("Error fetching poll:", error);
return null;
}
return poll;
}
export async function votePoll(
pollId: string,
optionId: string
): Promise