# 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 { 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 { 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 { 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 (

Huidige Polls

+ Nieuwe Poll
{polls.map((poll) => ( ))}
); } ``` 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 (

{poll.question}

{poll.options.map((option) => { const percentage = totalVotes > 0 ? (option.votes / totalVotes) * 100 : 0; return (
{option.text} ({option.votes})
); })}
); } ``` 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

Dank je voor je stem!

; } return (
{options.map((option) => ( ))}
); } ``` 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 (

{poll.question}

); } ``` 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 (

Nieuwe Poll

setQuestion(e.target.value)} className="w-full p-2 border rounded" placeholder="Stel je vraag..." required />
{options.map((option, index) => (
updateOption(index, e.target.value)} className="w-full p-2 border rounded" placeholder={`Optie ${index + 1}`} required />
))}
); } ``` --- ### 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)