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

552 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```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<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!"
```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 (
<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)
```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 (
<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)
```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 <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
```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 (
<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
```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:3011: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 (
<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)