fix: add 9

This commit is contained in:
2026-03-31 16:34:28 +02:00
parent b9ffee586f
commit 426b9f89d9
24 changed files with 1533 additions and 3758 deletions

View File

@@ -0,0 +1,551 @@
# 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)