# 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.
