fix: add 9
This commit is contained in:
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.
Reference in New Issue
Block a user