final lessons

This commit is contained in:
2026-06-07 10:44:05 +02:00
parent ceea2f206a
commit 39ec1bac72
105 changed files with 7789 additions and 1515 deletions

View File

@@ -0,0 +1,240 @@
# Les 8 — Lesopdracht
## CRUD + voting werkend met Supabase
**Duur:** ~120 minuten klassikaal
---
## Doel
Aan het einde: QuickPoll volledig werkend tegen Supabase. Polls aanmaken via /create, stemmen werkt persistent, alles in database.
---
## Stap 1 — /create pagina
`app/create/page.tsx`:
```tsx
import { createPoll } from "./actions";
export default function CreatePage() {
return (
<main className="max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">Nieuwe poll</h1>
<form action={createPoll} className="space-y-4">
<input
name="title"
required
placeholder="Vraag"
className="w-full border p-2 rounded"
/>
<textarea
name="options"
required
placeholder="Opties, één per regel"
rows={4}
className="w-full border p-2 rounded"
/>
<button className="bg-blue-600 text-white px-4 py-2 rounded">
Aanmaken
</button>
</form>
</main>
);
}
```
---
## Stap 2 — Create action
`app/create/actions.ts`:
```typescript
"use server";
import { supabase } from "@/lib/supabase";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
export async function createPoll(formData: FormData) {
const title = formData.get("title") as string;
const optionsText = formData.get("options") as string;
const options = optionsText.split("\n").map(t => t.trim()).filter(Boolean);
if (!title || options.length < 2) {
throw new Error("Vul titel + min 2 opties in");
}
// Insert poll
const { data: poll, error: pollErr } = await supabase
.from("polls")
.insert({ title })
.select()
.single();
if (pollErr) throw new Error(pollErr.message);
// Insert opties
const { error: optErr } = await supabase
.from("options")
.insert(options.map(text => ({ poll_id: poll.id, text })));
if (optErr) throw new Error(optErr.message);
revalidatePath("/");
redirect(`/poll/${poll.id}`);
}
```
---
## Stap 3 — RPC function voor voting
In Supabase SQL Editor:
```sql
create or replace function increment_vote(option_id_input bigint)
returns void as $$
update options
set votes = votes + 1
where id = option_id_input;
$$ language sql;
```
Test:
```sql
select increment_vote(1);
select * from options where id = 1;
-- votes moet +1 zijn
```
---
## Stap 4 — Vote action + VoteForm
`app/poll/[id]/actions.ts`:
```typescript
"use server";
import { supabase } from "@/lib/supabase";
import { revalidatePath } from "next/cache";
export async function vote(pollId: number, optionId: number) {
const { error } = await supabase.rpc("increment_vote", {
option_id_input: optionId,
});
if (error) throw new Error(error.message);
revalidatePath(`/poll/${pollId}`);
}
```
`components/VoteForm.tsx`:
```tsx
"use client";
import { vote } from "@/app/poll/[id]/actions";
import { useFormStatus } from "react-dom";
function VoteButton({ option, pollId }) {
const { pending } = useFormStatus();
return (
<form action={vote.bind(null, pollId, option.id)}>
<button
disabled={pending}
className="w-full text-left p-3 border rounded hover:bg-gray-50 disabled:opacity-50 flex justify-between"
>
<span>{option.text}</span>
<span className="text-gray-500">{option.votes}</span>
</button>
</form>
);
}
export function VoteForm({ poll }) {
return (
<div className="space-y-2">
{poll.options.map(o => (
<VoteButton key={o.id} option={o} pollId={poll.id} />
))}
</div>
);
}
```
---
## Stap 5 — Detail-pagina updaten
```tsx
// app/poll/[id]/page.tsx
import { supabase } from "@/lib/supabase";
import { notFound } from "next/navigation";
import { VoteForm } from "@/components/VoteForm";
export default async function PollPage({
params,
}: { params: Promise<{ id: string }> }) {
const { id } = await params;
const { data: poll } = await supabase
.from("polls")
.select("*, options(*)")
.eq("id", Number(id))
.single();
if (!poll) notFound();
return (
<main className="max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">{poll.title}</h1>
<VoteForm poll={poll} />
</main>
);
}
```
---
## Stap 6 — Test
1. Open `/create` → maak een nieuwe poll
2. Wordt geredirect naar `/poll/[nieuw-id]`
3. Klik op een optie → vote-count gaat omhoog
4. Refresh → vote blijft (persistent!)
5. Open Supabase Table Editor → zie de vote in DB
---
## Eisen
- [ ] Polls aanmaken werkt via /create
- [ ] Stemmen werkt en persistent
- [ ] Server actions met Supabase
- [ ] RPC function `increment_vote` bestaat
- [ ] Geen errors in console
- [ ] Vote-count update direct na klik (revalidatePath werkt)
---
## Bonus
- **Optimistic UI** — `useOptimistic` voor instant vote-feedback
- **Empty state** — als geen polls: mooie CTA naar /create
- **Disable vote per session** — sla in localStorage op wie heeft gestemd
- **Total votes** — toon onderaan totaal aantal stemmen
---
## Veelvoorkomende problemen
| Probleem | Oplossing |
|----------|-----------|
| "function increment_vote does not exist" | RPC nog niet aangemaakt — Stap 3 |
| Vote-count update niet | `revalidatePath` ontbreekt of verkeerde path |
| Form submit doet niets | `"use server"` mist in actions.ts |
| Foreign key error op options | Poll moet eerst inserten, dan opties |
---
## Klaar?
Volgende les: Supabase Auth — gebruikers laten inloggen.