Files
novi-lessons/Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md
2026-03-31 16:34:28 +02:00

13 KiB
Raw Permalink Blame History

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:1010:15)

Stap 1: npm install

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

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

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:

// OUD
const polls = [
  { question: "...", options: ["...", "..."], votes: [0, 0] }
];

export function getPolls() {
  return polls;
}

Dan: "Dit vervangen we door Supabase queries."

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!"

// 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)

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)

'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)

'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

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

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:3011:30)

Theorie op Beamer (15 min)

Toon INSERT query uitleggen:

// 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:

-- 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:

'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:0009:10: Welkom + Slide 1, 2, 3
  • 09:1010:15: Live Coding (Stap 111) + Slide 6 halverwege
  • 10:1510:30: Pauze (Slide 7)
  • 10:3011:30: Zelf Doen + Theorie (Slide 8)
  • 11:3011:45: Vragen
  • 11:4512:00: Huiswerk + Afsluiting (Slide 9, 10)