5.4 KiB
5.4 KiB
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:
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:
"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:
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:
select increment_vote(1);
select * from options where id = 1;
-- votes moet +1 zijn
Stap 4 — Vote action + VoteForm
app/poll/[id]/actions.ts:
"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:
"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
// 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
- Open
/create→ maak een nieuwe poll - Wordt geredirect naar
/poll/[nieuw-id] - Klik op een optie → vote-count gaat omhoog
- Refresh → vote blijft (persistent!)
- 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_votebestaat - Geen errors in console
- Vote-count update direct na klik (revalidatePath werkt)
Bonus
- Optimistic UI —
useOptimisticvoor 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.