diff --git a/Les06-Compleet.zip b/Les06-Compleet.zip deleted file mode 100644 index f6a9057..0000000 Binary files a/Les06-Compleet.zip and /dev/null differ diff --git a/Les07-Compleet.zip b/Les07-Compleet.zip deleted file mode 100644 index 2540962..0000000 Binary files a/Les07-Compleet.zip and /dev/null differ diff --git a/Les07-Compleet/Les07-Docenttekst.md b/Les07-Nextjs/Les07-Docenttekst.md similarity index 100% rename from Les07-Compleet/Les07-Docenttekst.md rename to Les07-Nextjs/Les07-Docenttekst.md diff --git a/Les07-Compleet/Les07-Lesopdracht.pdf b/Les07-Nextjs/Les07-Lesopdracht.pdf similarity index 100% rename from Les07-Compleet/Les07-Lesopdracht.pdf rename to Les07-Nextjs/Les07-Lesopdracht.pdf diff --git a/Les07-Compleet/Les07-Live-Coding-Guide.md b/Les07-Nextjs/Les07-Live-Coding-Guide.md similarity index 100% rename from Les07-Compleet/Les07-Live-Coding-Guide.md rename to Les07-Nextjs/Les07-Live-Coding-Guide.md diff --git a/Les07-Compleet/Les07-Slide-Overzicht.md b/Les07-Nextjs/Les07-Slide-Overzicht.md similarity index 100% rename from Les07-Compleet/Les07-Slide-Overzicht.md rename to Les07-Nextjs/Les07-Slide-Overzicht.md diff --git a/Les07-Compleet/Les07-Slides.pptx b/Les07-Nextjs/Les07-Slides.pptx similarity index 100% rename from Les07-Compleet/Les07-Slides.pptx rename to Les07-Nextjs/Les07-Slides.pptx diff --git a/Les08-Compleet-v2.zip b/Les08-Compleet-v2.zip deleted file mode 100644 index b07bc97..0000000 Binary files a/Les08-Compleet-v2.zip and /dev/null differ diff --git a/Les08-Compleet.zip b/Les08-Compleet.zip deleted file mode 100644 index dda5cc6..0000000 Binary files a/Les08-Compleet.zip and /dev/null differ diff --git a/Les08-Supabase+Nextjs/Les08-Docenttekst.md b/Les08-Supabase+Nextjs/Les08-Docenttekst.md new file mode 100644 index 0000000..7744e37 --- /dev/null +++ b/Les08-Supabase+Nextjs/Les08-Docenttekst.md @@ -0,0 +1,575 @@ +# Les 8 β€” Docenttekst +## Van In-Memory naar Supabase + +--- + +## Lesoverzicht + +| Gegeven | Details | +|---------|---------| +| **Les** | 8 van 18 | +| **Onderwerp** | Supabase koppelen aan Next.js | +| **Duur** | 3 uur (09:00 – 12:00) | +| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen | +| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account | + +## Leerdoelen + +Na deze les kunnen studenten: +1. De Supabase JavaScript client installeren en configureren +2. Environment variables gebruiken voor API keys +3. Data ophalen via Supabase queries (select met relaties, eq, single) +4. Het verschil uitleggen tussen sync en async data ophalen +5. Het Server Component + Client Component patroon toepassen +6. Een formulier bouwen dat data INSERT in Supabase + +--- + +## Lesplanning + +### 09:00–09:10 | Welkom & Terugblik (10 min) +πŸ“Œ Slide 1, 2, 3 + +**Doel:** Studenten op dezelfde pagina brengen over waar we zijn. + +**Wat te zeggen:** +- "Vorige week hebben we een werkend polling app gebouwd met in-memory data." +- "Vandaag koppelen we Supabase: onze database-as-a-service." +- "Na vandaag kunnen jullie niet alleen stemmen, maar ook nieuwe polls aanmaken." + +**Check:** +- Iedereen heeft Supabase account met polls en options tabellen +- Iedereen heeft QuickPoll project lokaal runnen op localhost:3000 +- Niemand heeft Auth ingesteld (dat doen we volgende les) + +--- + +### 09:10–10:15 | DEEL 1: Live Coding β€” Supabase koppelen (65 min) +πŸ“Œ Slide 4, 5, 6 + +**Doel:** Live voor hen de hele flow bouwen: installatie β†’ queries β†’ component aanpassingen. + +**Voorbereiding jij:** +1. Open Cursor met je QuickPoll project +2. Zorg dat Supabase dashboard open staat in je browser +3. `npm install @supabase/supabase-js` al gedraaid (zeker weten!) +4. Terminal gereed, dev server draait + +**Stap-voor-stap Live Coding:** + +#### 1. npm install @supabase/supabase-js +```bash +npm install @supabase/supabase-js +``` +**Zeg:** "Dit geeft ons de client om met Supabase te praten." + +#### 2. .env.local (Settings β†’ API) +``` +NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGc... +``` + +**Zeg:** "Dit zijn jullie API credentials. Ziet erruit in Supabase Settings β†’ API. De `NEXT_PUBLIC_` prefix betekent dat deze in de browser beschikbaar zijn (safe)." + +**Docent tip:** Na `npm install` en .env wijzigen moet de dev server **herstarten**! Zeg dit expliciet. + +#### 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! +); +``` + +**Zeg:** "Dit is onze Supabase client. We maken hem eenmalig aan en exporteren hem, dan kunnen alle componenten hem gebruiken." + +#### 4. types/index.ts (Database matching) +```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; +} +``` + +**Zeg:** "Dit matchen onze TypeScript types met de database schema. Poll bevat options als relatie." + +#### 5. lib/data.ts (Supabase queries herschrijven) + +**VOOR je dit toont, laat je het oude in-memory array zien:** +```typescript +// OUD: +const polls = [ + { question: "...", options: ["...", "..."], votes: [0, 0] } +]; + +export function getPolls() { + return polls; +} +``` + +**Zeg:** "Dit was in-memory. Nu halen we het uit Supabase." + +**NA - 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 op, EN daarbij hun relatie options" +- `.eq("id", id)` = "Where id = ..." +- `.single()` = "Ik verwacht exact 1 resultaat" +- `await` = Dit is nu async! Componenten moeten `async` zijn of we gebruiken een API route + +#### 6. PAUZE β€” Slide 6: Server vs Client: Wie doet wat? + +**BELANGRIJK:** Toon deze slide VOOR je componenten aanpast. Dit patroon is cruciaal. + +**Zeg:** +"We hebben nu async functies. Server Components kunnen `await` direct gebruiken. Client Components niet. Daarom splitsen we: +- Server Components: /page.tsx files (halen data op met await) +- Client Components: VoteForm (useState, onClick event handlers)" + +Laat code zien: +```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 <>{...} +} +``` + +--- + +#### 7. 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) => ( + + ))} +
+
+ ); +} +``` + +**Zeg:** "Dit is nu async! De `await getPolls()` werkt hier rechtstreeks. Link naar /create toevoegen." + +#### 8. 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}) + +
+ + ); + })} +
+
+ ); +} +``` + +**Zeg:** "Nu hebben we Option type beschikbaar. Percentage bars tonen stemmen visueel." + +#### 9. components/VoteForm.tsx (Client Component met vote mutation) +```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) => ( + + ))} +
+ ); +} +``` + +**Zeg:** "Dit is Client Component: `'use client'` bovenaan. We kunnen useState gebruiken, onClick handlers. Na stem, feedback tonen." + +#### 10. app/poll/[id]/page.tsx (Server + Client combo) +```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}

+ +
+ ); +} +``` + +**Zeg:** "Server Component haalt data. Geeft VoteForm (Client Component) de options door. Best of both worlds!" + +#### 11. app/api/polls/[id]/route.ts (GET + POST) +```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 }); +} +``` + +#### 12. Test alles! +- Homepage laden β†’ alle polls met opties tonen +- Click poll β†’ detail pagina, stem kan worden gegeven +- Stem geven β†’ votes increment in Supabase +- Controleer in Supabase dashboard β†’ votes kolom stijgt + +**Docent tips bij Live Coding:** +1. **TypeScript errors:** "Soms zien we rode squigglies. Dat is TypeScript die zegt 'ik snap dit type niet'. Hover je eroverheen, meestal is het een `!` die je moet toevoegen of een import." +2. **RLS blocking:** "Nog krijgen we misschien 'RLS policy violation'. Dat fix je volgende les met Auth. Nu gebruiken we publieke SELECT." +3. **Env restart:** Na .env wijzigen ECHT herstarten. Hardnekkig bug! +4. **Queries testen:** Open Supabase dashboard β†’ SQL Editor β†’ test je select statements daar eerst. + +--- + +### 10:15–10:30 | PAUZE (15 min) +πŸ“Œ Slide 7 + +--- + +### 10:30–11:30 | DEEL 2: Zelf Doen β€” /create pagina (60 min) +πŸ“Œ Slide 8 + +**Doel:** Studenten bouwen zelf een formulier om nieuwe polls aan te maken. + +#### Stap 1: Theorie op beamer (15 min) + +**Zeg:** +"Nu bouwen jullie zelf de /create pagina. Daarmee kunnen gebruikers nieuwe polls aanmaken. Eerst leg ik het uit, dan doen jullie het zelf." + +**INSERT queries uitleggen:** + +Laat dit zien: +```typescript +// 1. Insert poll +const { data: poll } = await supabase + .from("polls") + .insert({ question: "Wat is je favoriete taal?" }) + .select() + .single(); + +// poll is nu { id: 42, question: "Wat is je favoriete taal?", ... } + +// 2. Insert options (meerdere tegelijk) +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 } +]); +``` + +**Zeg:** +- ".insert() = INSERT statement" +- ".select().single() = geef me terug wat je net inserted, als 1 rij" +- "poll.id gebruiken we dan voor de options" +- "Daarna .insert([...]) meerdere opties in één keer" +- "Dan router.push('/') terug naar homepage" + +**RLS policy toevoegen:** + +Laat dit SQL blokje zien (ze moeten dit in Supabase doen): +```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); +``` + +**Zeg:** +"Dit zegt tegen Supabase: 'Iedereen mag INSERT-en op polls en options.' Zonder dit krijgen jullie 'RLS policy violation'. Dit is tijdelijk β€” volgende week beperken we dit met Auth." + +**Form outline:** +``` +1. Text input voor vraag +2. Meerdere text inputs voor opties (minimum 2) +3. "+ Optie toevoegen" knop +4. "Poll aanmaken" submit knop +5. Bij submit: INSERT in polls, dan INSERT in options, dan redirect("/") +``` + +#### Stap 2: Zelf doen (45 min) + +**Wat studenten moeten doen:** + +1. **RLS policy** in Supabase dashboard toevoegen (SQL Editor) +2. **app/create/page.tsx** aanmaken met: + - `'use client'` bovenaan + - useState voor question en options array + - Input voor question + - Loop over options, input per optie + - "+ Optie toevoegen" knop (addOption) + - "Poll aanmaken" button (handleSubmit) +3. **handleSubmit logica:** + - Insert poll β†’ krijg poll.id terug + - Insert opties met die poll_id + - Error handling + - router.push("/") na succes +4. **Homepage (page.tsx) updaten:** + - Link naar /create bovenaan + +**Docent loop ronde:** +- **Min 0-5:** Iedereen aan het werk? +- **Min 15:** Check of iedereen RLS policy heeft ingesteld. Help als iemand vast zit. +- **Min 25:** Toon code snippet van useState setup als mensen vragen hebben. +- **Min 30:** Check of eerste iemand INSERT werkend heeft. Toon in Supabase dashboard hoe je ziet dat poll aangemaakt is. +- **Min 45:** Ruim 5 min voor finalisatie, vragen, troubleshoot. + +**Veelvoorkomende problemen:** + +| Probleem | Oplossing | +|----------|-----------| +| "RLS policy violation" | Zeg: RLS policy toegevoegd in dashboard? Zien we in error message "RLS"? | +| "poll is undefined na insert" | `.select().single()` weg? Dat moet je toevoegen! | +| "Opties werken niet" | poll.id goed doorgegeven aan insert? Controleer in Supabase options tabel. | +| "Form submit refresh de pagina" | `e.preventDefault()` in handleSubmit? | +| "Redirect werkt niet" | `import { useRouter }` bovenaan? `const router = useRouter()` in component? | +| "Opties array gaat fout" | Laat code zien: `const newOptions = [...options]; newOptions[index] = value; setOptions(newOptions);` | + +--- + +### 11:30–11:45 | Vragen & Reflectie (15 min) + +**Mogelijke vragen + antwoorden:** + +**V: Wat happens na redirect?** +A: De homepage laadt opnieuw. `app/page.tsx` roept getPolls() aan, die hit Supabase en toont je nieuwe poll. + +**V: Waarom `async`/`await`?** +A: Supabase is over het network. We wachten tot het antwoord komt. `async` zegt "dit kan tijd kosten". + +**V: Kan ik realtime zien als iemand anders stemt?** +A: Volgende week! Supabase heeft realtime subscriptions. Daar leren we. + +**V: Wat is `/api/` folder?** +A: Dat zijn backend endpoints. Volgende week gebruiken we die meer. + +**V: Waarom `'use client'` in create en vote, maar niet in page?** +A: Client = interactief (forms, buttons, state). Server = data fetching. Next.js split dit automatisch. + +--- + +### 11:45–12:00 | Huiswerk & Afsluiting (15 min) +πŸ“Œ Slide 9, 10 + +**Huiswerk:** +1. **/create pagina afmaken** (als nog niet klaar in klas) +2. **Validatie toevoegen:** + - Vraag mag niet leeg + - Opties moeten uniek zijn + - Minimaal 2 opties + - Error messages tonen +3. **Delete functionaliteit:** + - Delete knop op PollItem + - Verwijder poll + opties uit Supabase +4. **Extra (voor snelle studenten):** + - SQL queries schrijven (direct in Supabase SQL Editor) + - Realtime subscriptions uittesten + - Styling verbeteren + +**Zeg:** +"Volgende week: Supabase Auth. Jullie gaan inloggen en registreren bouwen. En bepalen wie welke polls mag aanmaken. Tot dan!" + +**Slide 10: Afsluiting** +- "Tot volgende week!" +- "Volgende les: Supabase Auth β€” inloggen, registreren, en bepalen wie wat mag" + +--- + +## Veelvoorkomende problemen & Troubleshoot + +| Symptoom | Oorzaak | Oplossing | +|----------|---------|-----------| +| "Cannot find module @supabase/supabase-js" | npm install niet gedraaid | `npm install @supabase/supabase-js` | +| Env vars undefined in browser console | NEXT_PUBLIC_ prefix vergeten OF dev server niet restarted | Restart dev server (`npm run dev`). Check prefix: NEXT_PUBLIC_SUPABASE_URL | +| "RLS policy violation" on SELECT | RLS enabled, geen SELECT policy | Voor nu: disable RLS in Supabase (Security β†’ RLS β†’ toggle OFF). Volgende les met Auth | +| "RLS policy violation" on INSERT | Geen INSERT policy of RLS restrictief | Voeg INSERT policies toe (zie Deel 2 stap 1) | +| getPolls() returns empty array | Query failed maar geen error | Check: .select() syntax correct? options(*) geindent? Controleer in Supabase SQL Editor | +| TypeScript "Cannot find name 'Poll'" | Import weg | `import { Poll } from "@/types"` bovenaan | +| "notFound() is not defined" | Import weg | `import { notFound } from "next/navigation"` | +| Percentage bars werken niet | totalVotes = 0 dus percentage = 0 | Check: votes kolom in Supabase β‰  0? Stem eenmalig via UI | +| Client form not submitting | e.preventDefault() weg OF loading state blocked | Check handleSubmit: eerst `e.preventDefault()`, geen return-statements die vorig breken | +| Redirect naar / werkt niet na poll maken | router niet geΓ―mporteerd OF router.push() fout | `import { useRouter }` from "next/navigation" (niet "next/router"!) | +| Supabase queries slow | Network latency / veel data | Normal! Later: replication, caching, realtime | + +--- + +## Tips voor docenten + +1. **Code live typen, niet copy-paste.** Laat typos zien, laat debugging zien. Authentiek! +2. **Veel pauzes voor vragen.** Live Coding voelt snel. Check regelmatig: "Allemaal met me mee?" +3. **Zelf Doen starten met duidelijke steps:** (1) RLS policy, (2) page.tsx, (3) form, (4) submit. Niet: "Bouw de pagina." +4. **Loop ronde, spot problemen vroeg:** Min 15-25 zijn goud voor troubleshoot. +5. **Toon Supabase dashboard often:** "Zie je? De data staat echt in de database!" +6. **Authenticatie is volgende les:** Zeg het af te toe: "Dit beperken we volgende week met Auth." +7. **Celebrate wins:** Eerste student met werkende /create? Toon het aan iedereen! diff --git a/Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf b/Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf new file mode 100644 index 0000000..839d14e --- /dev/null +++ b/Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf @@ -0,0 +1,238 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 7 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +7 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +8 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +14 0 obj +<< +/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 17 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +15 0 obj +<< +/PageMode /UseNone /Pages 17 0 R /Type /Catalog +>> +endobj +16 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20260331162605+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331162605+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +17 0 obj +<< +/Count 9 /Kids [ 4 0 R 5 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R ] /Type /Pages +>> +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 697 +>> +stream +Gatm8bAu;j']&X:mP8J]`Hk_QY+>2NLF/kfO:IA/tE`'8e[+s7rBa#J;Pke9tMU'rjQTHqpLij)teCgHVCrYQd)md$_NEmTQLLJ5l3Vl2SCB;AcE',lK4nH:I^DF]-M;G?I-rDaOLN#@Q3I3/[E\nj:N0-QoSr!%JFlVR>[6Q;4W.&"5kLV.=doh;V]80D/mgb9k*qp.'1eBSL*TtMDREt`O;,?;>nfG3V&jHF&^A>qT*O`sE`MDbnlR>03>$LP)M3NNB?Fq>W"&o"'4?LTAbTqD(nfk'.A5b952mP\YIOK<8.j)iBN2AkkD&>n1dVQOf]F`+!%3oZ.glXX;#KLJb(2R>ChB][5tXiYqoDrE0g2Ha64PNaVoi1\e>m3,0$@,7+O1Di/%gQmdAN+*tb.8BbJ=n4Y*B;RD:X@K]6B^0I3#h:]8j5mtWhGGXGV$<2U>pF.[NN\0'A,-(%/>ERHHF%VSQFr"8JClQi#gKsmK7RfW*+,;^!B,7mgO*Jdf"1G-2YQq=OZJ0'*RM,Q1)8ch3]gQJ%uEN;TcO'EFUO[7@(;W$Msj*#TqB>7l&*#Gf14'agjI)u4L5+7kPL~>endstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 772 +>> +stream +GauIu?#Q2d'F*Lmr/,Q`DA'qN.pQt-W^_lGY!KagEoW6[il#5>$sil2$q/q&+jH2t#72RXEZQ1SY6k!d+N$2m!Bt.8I7o)eaI[s9?3o4&QJ(V97P8o)e;I7o&kJb8,AE[:kKM6nNG0"phusKQ81Adtd!n:q1\77"QSM]X8l20JM>k/Bq5QKsG2LBP?l`Q>G(f3L?hUN\VFh)EkL1RA92FG1(2l.Er!j!Z!/^QX4bJifi=Xs(L&>pj"jClr"craBFNsMFJMZk\`,/s4P;1cXX^"qIT#r1NhiZ`utKF_Qh*l^]T3Ad.mH^1L)L,u'1gBei:&U_#&9-02A5AS"?3Kf&@+8f+8p8SYo'lJJBoFX^%7p*o?*1G6J%!a[ShWGe,:s!o#cOo"t8esO%`i!e%($69I;R!Au,b(cSfCJ&=eS0@#4>FZHC2u/F^"L*ar_'$4_JE<23FVU`INlrO~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1140 +>> +stream +Gatm:mr-r=&H2%3imLV?;:?lb4%4ZGR9-3WgoOP6%1!M!16]kC6rd^OAAeE@*\#33*.([e>D?fY,k'RLHZu2<_Mak0PQIA.0`6n3AJ!\u[>>@O"NFm=997X[>t3V""Va)>,;a?A>_e@HF.$Z&5V.Cq"Umq0kmnK;4ku70_Ls@m@),GAI@ie)=!83LLG6?dq)'f/grVYpM9(5=%&W#MO[]'Y_?GUr1n6f"g'5Z.D+Jc%Ijf,nRultD&nXj4erG;o+jt[R;![VET?:1^[kP,i*/nK']6RkJ/JCHBi1r-4,>i/^Zkahfj8>WK2^c*.T'[M=C>/=QC)^Vc7,DiT3?9CQEQ&6rG-q*cWr2r-X+7Q]PHr^If"jFq(hE/I4Z`&\$oc,D3"QY8ALe&P0!,L%A=?k0TK['&IgE(@mCQAVdaRa9$*&3ii#/:)j^-d9Q#.S:hi8g/[qh$G%&/Z$26UI*:bmD0=A^/&qq(_(WHC$XFqR\'8"LNHg-Uk:4;<)I1(G\YV)(g+GoBSf/-6%NVg-JGnl`ZF6kGltP=uX'u6iW>a[osefPnuda1Zh8cQ5*Rc#*?sKCtC$Y>EVBD4K4r0JbgD*j]HDYeNIZjAh@j_+bb5s'rtf?f$Su01*-B4?CbGn9[6D:Af'XN-c`R`e]#SNd:]]54,b,M9$"HkOD]@faUOK6[QbB`s%t*p=P)r*@VQtWnatYL>'J8QK>TTgS6t8H^?te^#R02)5k"r'`=^Pa-AWdcJi3@.:`c=9ToA19qbq?GdDFk;5/:hRmn%V0JmS'kXg`Vrn?>]!Gik)&s4S8;^ci)PCqJ6g8h2&#OD=@?bYcDeC5e>Oghq'17tZqm)__?AY\`La,YA9c]QljhI01+!qLEh\M-=A\OU@%_`4-DLMc2$Q1%u;]*;endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 904 +>> +stream +Gb"/%>Aoub'Ro4H*:8SNSJ-&PA/@t&g2596[4?0lW[KSN'nO;Hej`C6qeSVuaH^l#gQ[Vei,d3,cfb0*6W^q)qMrPq"!9R:(pF$i%+ml^pcm]df;9H==#pP+4N#'EHTd$F`tNIP13,OeCO33Grq`W<)]gis3Sg9B_:a%9#d,T.sd%O2&O^C<6&l/j-,p*#obD[HBh/7:L`N$io%#KCJ@N>dO?P_>P8DaJn\?p9_'=6N.i;'>a:b_,6^b*m2=fS.g'RR@T!I`/0mqr!V&-lcn=n;:dKBg&.CfeTD(/oHKs646i-icE7OX2_BQ>12Uak.L8l(Z*:)25n664(!YO/$u+1[5TWd=<(%\HN&gCd\TH-#,N"S"%$PfQ@JbB,;^K2s)>7EXZp_8g#HQ`mQMq<+WP&..ff:utiP!226I!q@WD+2W[uD5V`%m2D'3+jb$%QMscp,%r!j&gJi^pe%[^$'\1(kj>\"CXn[`f]f+b"M3SABJh)]hZ;de5t/pWkYlUD1#^NNn2F0kokQ)#,HL7jbt'8A1R^Vs%^^$MF*#dPe?[2(Z%*9B\_YQY23DTWk#S\*Nji84jd;eGl~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1296 +>> +stream +GauHK;01_T&:WeDm.BD`orsQqN8^4!-_7&W1[.X%9)DM1DM/V?LFr=44)gDB^NV7K(D@'d+;mKkRFUTO\(XVn!aY^8p"tf1Y!ugS&0NP/5]'en=W6O@Y(gfR4T*5cbrlEFZg-#HEt?+sB`c3n15J6l*Z:c-Ue^pX::qCT`%bF@j+0.Gqlg^8(E1<>L9$f[^5K-:JuE?-Dcg.Jd\4Z)PiI*0ce_rkq_rqXg%c'BQ@cn$WOOsn!^ET>Z:Fr)W5ej)fOk\jgEuIQ3XOE.[8q79VV,&74O%C[0)+hU#[JRiIA3D=.SY_MRHc1rj&V_["K?9#_A-K&O%$Vutj9D#B$3I)8*2!?suaoJbiDkbb1bfl9p\1rOo8dO37h6]CF)?\A-n4^/!+%4%V3b+7*q7n9N,)QP:4?\Y7BA4AuC+J$g]mE1oUC`PTq>FAWTd_L:i_o(F&]?07SFjG02ICa&pmZG's6q[1p@E-`#kn6sf.I%h9amlDD#%IbGUU?WF3.@]ZgORUc1HA[:318)'9[$T:R'LC@fo#+6=Bk-3Un!_H4^h9XKhleQ5D44tAl!1p1kd9sMNbFhY=Zn8*2.FbhNj%@P/*=6bTqE55GUmAM+6j+7a.S_c,(#9]'Lt/jq<$a%a@lPEQ>$r?Cm1S!Rc>#%qph*UgkR`WShK'/A_]]j..$p%p:Rm:dYrm2>>*RH-P0D;gpCQ[([9mQOMuPI.lo]a'ltK::CW*![VEFnHkDH,u!\H`hDnb3uE4II6H3[%%dTe/IL3=KjNfJdYepupS1>s4ea[nZBc"t)9Qo(Sg@<2UnX$k3W!fid-AGXV=^]'Y/#8sVS'@h?CegK3l7h#8&;?nAIV6PdrBN>fendstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 474 +>> +stream +Gau1(btc/1&;9M#ME(kN8N2lu*'mDS"JUFcKlA`jP,@:Na)1(qA%T)^."VQB;A(M?R9>#mY$'hWUCoO!#R1D=+W`p4^B\b='@D*nd$lctr?r.c69cZdi[?_Jb\s,bAjc@kpXXp\i^hlOaMZsSY3W(LT5-oCd70&oi[:5N0l30fQ0i03IXtV,`p&=6,VZmD&K&\@/QfF[EbiJ9=ZpK+R`dIi18ETSVHDGK<>e>fpHq-df]eah5=7g$'mB]=f)~>endstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1326 +>> +stream +Gau0CD0+E#&H9tYf]acRYn*BNKLD`enI'6)\fh)K1MuAu%^]Ws6;jTm,k:ZEGBK'na+cVYL(h/3gLm/7HWG+4kfqZPBF8_;6K=L:f>/I02jk!?/Tfe%k8H!*/Y-%)mVlhs>DtY06gJESl'C0]S%oG:1E7F&A)o:6-8jM@YIJSMj?a)W\]DO(Tmh\4JDQ%#-p]!g@PRC?!Q-=T@sA(C4h,EN(a&"U$]:X]6K&j]i354Z688ir'J3D62AMH@0c7f.KHUbG]`j]+;a8Le5qmOV?0*b`)HaGQ0K:&M?;\]oL!s'^Dr14M4Vp:pR/!&_%*l/cs585_8)5U:&JY(4q11L4!Y?OY9&*B.d)6'B8`i7Q4bB1K_E-9Ab@&SlZO59,ntL`\Z1gnN2.9g$Nhs."'/i>f-A40:s-RtnfsD00$7%pIGbZ7^`QE$JO-U`1N/_TTS35m29L6A>!7'LL9JMZe"r18^>(%*=WJ!e2KGnC'L:@kili=-QQ27.>9^$(_Y#P%&qdnoFL,5F_kD0DKOl6EMGp'n$39b_T-RCP/UY*fn#P:4Y,(:Kp$T73\Kdh_p0SI>XD:8ED%8Jq8FOYStc4.X*2f?S9bX?rFpp&T[F;@G/D,f1aVr,VHLTg8$$NG/*E*Q*8EHpr6]Y58(qXIVer"@J?2J.BL/@o_bmn3H.:VPH?;RCo;f6kJETHDFLW6kclPLr5Iql[UZ`MeQ@1^[Q9H0+;/;1>USU`9<4ba9DNTaL-!cYJeV'D'oQu[gFtHb#7HF;I?"WBtjuE;"QdP<8#kl*1*$2KF+&?^Q^(qQ?C//@:*PX7LpSi.^qhPCB"@Fa_kV~>endstream +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 749 +>> +stream +Gat$ubAQ&g&A7lj#.[-9$TXGYE/m5J[Qp=jZ-Z/""(eTG`Y,84PhXOQMO#?(;?V[RMd"EO/=3[Y\E$/5haI]RVs*"FL]lrT@MfW+9HgHS!3DST(1n!oD15s1o4QS]^k!ZK;RW;R80kumSSS77sEQNIqltm+u,0"og#`DTCp?0N@L:!0`oB8,A79N(u%%)>)$+qVApK1"XQXh,Da]1(1\3TljUi-ej0iIl.Tlf+8ClGX?#obC+E<$GWHm,dU=b>P+h%jmCYK1\aQ-u[qZ:&^a0T9ue^7r+3g-Xa-D%%/T\)B-E[AdRa<33@"@h;;l#T22`Ha[ji/AW^;jSH[QR]KCN&0>l30s9%o[s*O%GI*e9P`L?`.GR#GgqH.[eMk&I>KW5-Ka7A)U4;*k`43(<:EMWuFE,87)i^\.HpqVl&T5<6h2]$DY&)ZQ0JEN3jZ>[[fjuuLX4AmV!sXB@i3E#pZTPXaBV!_h9csXEbI4"#4t>sUlSXYkacYWthXQ)"GV1#@@\h42cB;c!2W,Qf#hG4;Ja%4?tt`i\B#2[\;@s2ik#_E;t2g(`qU'g,2[G#dd\;<8R'?/^Sbl:Yh&f?Wd5L9E~>endstream +endobj +26 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 804 +>> +stream +Gatm89lldX&A@sBCi?,o&51ZqjOR)^kolTm)oa74"DimQOGFHfgL&i,Nap*O'5(Jc&.H2>]j74q_2qFSms,B3.>=fU$lFcl%bN0L6$`X>FRUtl:QZ;3:(j-PLrXMVP"@ucoJttG*fn;P7ake1E!VZ+5JWQe_IP_bBh]9]\>SQZ2\:B(A@k04>sPuSoR5'\K?du.dF:DRXN(EfoGg-%fT7pFfO+AaNAe'Zo9GBMX?#!(ao("$^FkJfhr/ta*[a,I)Uio^F.AO#!=Q_"DA<=V\OGHg)Gs*Y5hsna6N%V+_$iT-(7#50U4Z&E@::HcO;\H/KrpF)$OQSO`EXfo#U^4S_+0,(lK^S7T=Rc[^+)]E`Tt"GVl5-eYj0Klad-TF`28>L4.)Shhl^$pcmR2)&<*@\Ee8)UsW^0LXI3QGJSPu:A(lW2@X2;$)f>Z]i"&-*mq.OZ9[a5iPWKm4lYb]+#i]9U#!X-n2Y=@-3=Ng0mESN\a:;tM&)ciQke$e]B+kZH%)Ja:=dqi/o,MYS%d5_PSmMB=`#VS-FY]):,Sdke[XL/lE]kH7U<;i7Y#+oj&c2N(JR8da@P[Rll[$@Xe[Sp0EgFpOH&:ap)PILF25M$FreKHE6R)c)E-mn[6J\`D4<_4mXAH5eB4#?E_3A`Aa=0iM+aL9RUIETXXb<"/)gC?.LbTdHR6Sf0.`sendstream +endobj +xref +0 27 +0000000000 65535 f +0000000061 00000 n +0000000122 00000 n +0000000229 00000 n +0000000341 00000 n +0000000546 00000 n +0000000751 00000 n +0000000856 00000 n +0000000971 00000 n +0000001176 00000 n +0000001381 00000 n +0000001587 00000 n +0000001793 00000 n +0000001999 00000 n +0000002205 00000 n +0000002411 00000 n +0000002481 00000 n +0000002762 00000 n +0000002875 00000 n +0000003663 00000 n +0000004526 00000 n +0000005758 00000 n +0000006753 00000 n +0000008141 00000 n +0000008706 00000 n +0000010124 00000 n +0000010964 00000 n +trailer +<< +/ID +[<136ccfea98a4fe3d25db4179196a9d43><136ccfea98a4fe3d25db4179196a9d43>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 16 0 R +/Root 15 0 R +/Size 27 +>> +startxref +11859 +%%EOF diff --git a/Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md b/Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md new file mode 100644 index 0000000..f75439e --- /dev/null +++ b/Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md @@ -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: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) diff --git a/Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md b/Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md new file mode 100644 index 0000000..8010a02 --- /dev/null +++ b/Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md @@ -0,0 +1,169 @@ +# Les 8 β€” Slide-overzicht +## Van In-Memory naar Supabase (10 slides) + +--- + +## Slide-indeling + +### Slide 1: Titelslide +**Titel:** Les 8 β€” Van In-Memory naar Supabase +**Ondertitel:** Koppelen van Supabase aan Next.js +**Afbeelding:** Supabase + Next.js logo's + +--- + +### Slide 2: Terugblik vorige les +**Titel:** Terugblik β€” Waar waren we? + +**Bullets:** +- Stemmen werkt lokaal (in-memory data) +- QuickPoll app heeft 2 pages: / en /poll/[id] +- VoteForm component ziet stemmen onmiddellijk +- Nu: alles naar een echte database + +**Code snippet (links):** +```javascript +// OUD +const polls = [ + { question: "...", options: [...], votes: [...] } +]; +``` + +--- + +### Slide 3: Planning vandaag +**Titel:** Planning β€” Les 8 (3 uur) + +**Timeline:** +- 09:00-09:10 | Welkom & Terugblik (10 min) +- 09:10-10:15 | **DEEL 1: Live Coding β€” Supabase koppelen** (65 min) +- 10:15-10:30 | Pauze (15 min) +- 10:30-11:30 | **DEEL 2: Zelf Doen β€” /create pagina** (60 min) +- 11:30-11:45 | Vragen & Reflectie (15 min) +- 11:45-12:00 | Huiswerk & Afsluiting (15 min) + +--- + +### Slide 4: Van Array naar Database +**Titel:** Van In-Memory Array naar Supabase + +**Links:** In-memory (OUD) +```javascript +const polls = [ + { question: "Favoriete taal?", + options: ["JS", "Python"], + votes: [10, 5] + } +]; +``` + +**Rechts:** Supabase Database (NIEUW) +``` +polls tabel +β”œβ”€ id (1) +β”œβ”€ question ("Favoriete taal?") +└─ options[] (relatie) + +options tabel +β”œβ”€ id (1) +β”œβ”€ poll_id (1) +β”œβ”€ text ("JS") +β”œβ”€ votes (10) +``` + +--- + +### Slide 5: Live Coding Deel 1 β€” Supabase Γ— Next.js +**Titel:** Live Coding β€” Deel 1: Supabase koppelen + +**Ondertitel:** Stap-voor-stap + +**Stappen:** +1. npm install @supabase/supabase-js +2. .env.local (API keys) +3. lib/supabase.ts (client) +4. types/index.ts (Poll + Option) +5. lib/data.ts (queries herschrijven) +6. app/page.tsx (Server Component) +7. components/PollItem.tsx (percentage bars) +8. components/VoteForm.tsx (Client Component) +9. app/poll/[id]/page.tsx (detail) +10. app/api/polls/[id]/route.ts (API) +11. Testen! + +**Spreaker:** "We werken samen naar een werkende Supabase integratie." + +--- + +### Slide 6: Server vs Client: Wie doet wat? +**Titel:** Server vs Client: Wie doet wat? + +**Twee kolommen:** + +**SERVER Component:** +- `export default async function HomePage() { ... }` +- `const polls = await getPolls()` βœ“ +- Data fetching +- Direct naar database +- TypeScript compile-time + +**CLIENT Component:** +- `'use client'` +- `const [voted, setVoted] = useState(...)` +- Interactief: klikken, typen, formulieren +- useEffect, event handlers +- Browser runtime + +**Zeg:** "Server haalt data, Client maakt het interactief." + +--- + +### Slide 7: Pauze +**Titel:** Pauze + +**Tekst:** Supabase is gekoppeld! Na de pauze: /create pagina bouwen + +**Icoon:** Koffie/pauze emojis + +--- + +### Slide 8: Zelf Doen β€” /create pagina bouwen +**Titel:** Zelf Doen + +**Ondertitel:** /create pagina bouwen + +**Stappen:** +1. **RLS INSERT policy** toevoegen in Supabase dashboard +2. **Form bouwen** met vraag + minimaal 2 opties +3. **Insert logica:** Eerst poll, dan options met poll_id +4. **Redirect** naar homepage na succes +5. **Link toevoegen** op homepage naar /create + +**Docent zegt:** "Zelf doen, 60 minuten. Ik loop rond!" + +--- + +### Slide 9: Huiswerk +**Titel:** Huiswerk + +**Verplicht:** +- /create pagina afmaken (als niet klaar) +- Validatie toevoegen (vraag niet leeg, min 2 opties) + +**Extra:** +- Delete functionaliteit +- SQL queries direct in Supabase testen +- Realtime subscriptions uittesten +- Styling verbeteren + +--- + +### Slide 10: Afsluiting +**Titel:** Tot volgende week! + +**Voorkant:** +- "Volgende les: Supabase Auth" +- "Inloggen, registreren" +- "Bepalen wie wat mag doen" + +**Achtergrond:** Supabase Auth afbeelding diff --git a/Les08-Supabase+Nextjs/Les08-Slides.pptx b/Les08-Supabase+Nextjs/Les08-Slides.pptx new file mode 100644 index 0000000..203339f Binary files /dev/null and b/Les08-Supabase+Nextjs/Les08-Slides.pptx differ diff --git a/Les08-Supabase-Auth/Les08-Docenttekst.md b/Les08-Supabase-Auth/Les08-Docenttekst.md deleted file mode 100644 index 6b8a976..0000000 --- a/Les08-Supabase-Auth/Les08-Docenttekst.md +++ /dev/null @@ -1,893 +0,0 @@ -# Les 8 β€” Docenttekst -## Supabase Γ— Next.js + Auth - ---- - -## Lesoverzicht - -| Gegeven | Details | -|---------|---------| -| **Les** | 8 van 18 | -| **Onderwerp** | Supabase koppelen + Auth introductie | -| **Duur** | 3 uur (09:00 – 12:00) | -| **Voorbereiding** | Werkend QuickPoll project, Supabase project met polls/options tabellen, RLS ingesteld | -| **Benodigdheden** | Laptop, Cursor/VS Code, browser, Supabase account | - ---- - -## Leerdoelen - -Na deze les kunnen studenten: -1. De Supabase JavaScript client gebruiken in een Next.js project -2. Data ophalen via Supabase queries (select met relaties, eq, single) -3. Het Server Component + Client Component patroon toepassen -4. Uitleggen wat authenticatie vs autorisatie is -5. Supabase Auth functies gebruiken (signUp, signIn, signOut, getUser) -6. Een login/registratie flow bouwen in Next.js - ---- - -## Lesvoorbereiding (voor docent) - -Zorg dat je volgende zaken hebt voorbereiding: -- Een werkend Supabase project met `polls` en `options` tabellen (uit Les 7) -- RLS ingeschakeld op beide tabellen met policies voor SELECT (anon) en UPDATE (anon op options) -- De Next.js QuickPoll app uit Les 7 werkend op je machine -- De slides gereed voor uitleg authenticatie vs autorisatie -- Test je eigen Supabase credentials vooraf - ---- - -## 09:00–09:10 | Welkom & Terugblik (10 min) - -**Doel:** Studenten krijgen duidelijk wat we vandaag doen en waar we van vorige week waren. - -### Wat we hebben gedaan in Les 7: -- βœ… Stemmen werkend gemaakt (votePoll functie, state update in poll detail page) -- βœ… Supabase introductie: account aangemaakt, project gemaakt -- βœ… Database: polls + options tabellen aangemaakt -- βœ… Foreign keys + CASCADE ingesteld -- βœ… RLS policies ingesteld (SELECT voor anon, UPDATE voor anon op options) -- βœ… Testdata ingevoerd via Table Editor - -### Wat we NIET hebben afgemaakt in Les 7: -- ❌ Supabase is NIET aan het Next.js project gekoppeld -- ❌ Data wordt nog niet uit Supabase opgehaald -- ❌ Geen authenticatie - -### Vandaag gaan we: -1. **DEEL 1 (65 min):** Supabase client installeren en opzetten β†’ data uit database halen in Next.js -2. **DEEL 2a (30 min):** Uitleg over authenticatie, autorisatie en Supabase Auth features -3. **DEEL 2b (30 min):** Studenten bouwen auth zelf in hun project (signup, login, logout) - -**Motivatie:** "Tot nu toe zijn je polls hardcoded in geheugen. Straks halen we echte data uit Supabase en kunnen people inloggen. Dat is een echt web app!" - ---- - -## 09:10–10:15 | DEEL 1: Supabase Koppelen β€” Live Coding (65 min) - -Dit deel volgt een stap-voor-stap aanpak met live coding. Alle studenten coderen mee. - -### 09:10–09:15 | Installatie (5 min) - -Open terminal in het QuickPoll project en run: - -```bash -npm install @supabase/supabase-js -``` - -**Teacher Tip:** Controleer dat de installatie slaagt. Als students `npm ERR!` zien, laat ze eerst `npm clean-install` doen en daarna opnieuw proberen. - -### 09:15–09:25 | Environment Variables (10 min) - -Zorg dat alle studenten hun Supabase credentials veilig opslaan. - -1. Open in Supabase Dashboard: **Settings** β†’ **API** -2. Kopieer: - - `Project URL` (eindigt op `.supabase.co`) - - `anon` public key - -3. Maak/open `.env.local` in je Next.js project root: - -```env -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here -``` - -**Belangrijk:** -- `.env.local` staat al in `.gitignore` (check even) -- Keys die beginnen met `NEXT_PUBLIC_` zijn zichtbaar in browser (maar anon keys zijn daarvoor bedoeld) -- **ALTIJD de dev server herstarten na wijzigen van `.env.local`** (Ctrl+C, dan `npm run dev`) - -**Teacher Tip:** Dit is een veelvoorkomende fout. Zeg hardop: "Als jullie een leeg array zien in plaats van polls, check EERST of je dev server herstarten hebt!" - -### 09:25–09:35 | Supabase Client aanmaken (10 min) - -Maak `lib/supabase.ts`: - -```typescript -import { createClient } from '@supabase/supabase-js' - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL! -const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - -export const supabase = createClient(supabaseUrl, supabaseKey) -``` - -**Wat gebeurt hier:** -- We importeren `createClient` uit `@supabase/supabase-js` -- We halen URL en key uit environment variables -- We geven deze aan `createClient` -- We exporteren de client zodat we het overal kunnen gebruiken - -**Teacher Tip:** TypeScript geeft mogelijk een warning over "null assertion (!)" β€” dat is OK. Dit zeggen we tegen TypeScript: "Deze values bestaan echt, vertrouw me." - -### 09:35–09:45 | Database Types (10 min) - -Maak `lib/types.ts` handmatig: - -```typescript -export interface Poll { - id: string - created_at: string - question: string -} - -export interface Option { - id: string - poll_id: string - text: string - votes: number -} -``` - -**Waarom:** Dit helpt TypeScript begrijpen welke data we uit Supabase krijgen. - -**Teacher Tip:** In een echt project zou je `npx supabase gen types typescript` gebruiken, maar dat kost extra setup. Voor deze les is handmatig OK. - -### 09:45–10:00 | Async Data functies (15 min) - -Update `lib/data.ts` β€” alle functies worden nu async en halen data uit Supabase: - -```typescript -import { supabase } from './supabase' -import { Poll, Option } from './types' - -export async function getPolls(): Promise { - const { data, error } = await supabase - .from('polls') - .select('*') - .order('created_at', { ascending: false }) - - if (error) { - console.error('Error fetching polls:', error) - return [] - } - - return data || [] -} - -export async function getOptions(pollId: string): Promise { - const { data, error } = await supabase - .from('options') - .select('*') - .eq('poll_id', pollId) - .order('votes', { ascending: false }) - - if (error) { - console.error('Error fetching options:', error) - return [] - } - - return data || [] -} -``` - -**Wat betekent dit:** -- `.from('polls')` β€” welke tabel -- `.select('*')` β€” alle kolommen -- `.eq('poll_id', pollId)` β€” filter op poll_id -- `.order()` β€” sorteer op -- `await` β€” wacht op het resultaat van de database call -- Error handling β€” log en return empty array - -**Teacher Tip:** Veel students maken hier fouten met async/await: -```typescript -// ❌ FOUT: promise niet awaited! -const data = supabase.from('polls').select('*') - -// βœ… GOED: -const data = await supabase.from('polls').select('*') -``` - -### 10:00–10:10 | Homepage als Server Component (10 min) - -Update `app/page.tsx` β€” dit wordt een Server Component: - -```typescript -import { getPolls } from '@/lib/data' -import PollItem from '@/components/PollItem' - -export default async function Home() { - const polls = await getPolls() - - return ( -
-

QuickPoll

- -
- {polls.map((poll) => ( - - ))} -
- - {polls.length === 0 && ( -

Geen polls beschikbaar.

- )} -
- ) -} -``` - -**Belangrijk:** Page.tsx is nu een **Server Component** β€” geen `'use client'` directive! We kunnen hier `async/await` rechtstreeks gebruiken. - -**Teacher Tip:** Students vragen: "Maar hoe krijgen we de options?" β€” Goed punt! Die halen we in PollItem. - -### 10:10–10:15 | PollItem Component (5 min) - -Update `components/PollItem.tsx` β€” ook een Server Component: - -```typescript -import { getOptions } from '@/lib/data' -import VoteForm from './VoteForm' -import { Poll } from '@/lib/types' - -export default async function PollItem({ poll }: { poll: Poll }) { - const options = await getOptions(poll.id) - - return ( -
-

{poll.question}

- -
- {options.map((option) => ( - - ))} -
-
- ) -} -``` - -**Waarom twee Server Components?** -- `page.tsx` ziet alleen alle polls (geen details) -- `PollItem` wordt per poll gerenderd en haalt zelf de options op (parallel!) -- Dit patroon is efficient en schaalbaar - -**Teacher Tip:** Dit is het "Suspended Components" patroon van React 18 β€” Server Components voeren dit automatisch in parallel uit. - ---- - -## 10:15–10:30 | PAUZE (15 min) - -Goed moment om even weg te lopen. Tussendoor kun jij: -- Rondlopen en kijken wie nog errors heeft -- Checken of iedereen env vars juist ingesteld heeft -- Dev servers herstarten voor wie vergeten zijn -- Voorbereiding treffen voor DEEL 2 - ---- - -## 10:30–11:00 | DEEL 2a: Uitleg Auth (30 min) - -Dit is uitleg β€” geen live coding nog. Zorg dat alle laptops dicht zijn, focus op slides en beamer. - -### Authenticatie vs Autorisatie - -**Authenticatie (Authentication):** -- "Wie ben je?" β€” identity verification -- Voorbeeld: Je logt in met email + password -- Supabase Auth zorgt hiervoor - -**Autorisatie (Authorization):** -- "Wat mag je?" β€” permissions -- Voorbeeld: Je mag alleen je eigen polls aanpassen -- RLS (Row Level Security) in Supabase zorgt hiervoor - -**Voorbeeld:** -- Auth: "Je email en password kloppen, je bent Alice." -- RLS: "Alice mag haar eigen polls zien en updaten, maar niet die van Bob." - -### Supabase Auth Features - -Demo op beamer: -1. Open Supabase Dashboard β†’ **Authentication** β†’ **Providers** -2. Toon dat **Email/Password** is ingeschakeld -3. Toon de instelling **"Confirm email"** (nu UIT voor dev) -4. Ga naar **Users** tab β€” hier zie je ingelogde users - -**Supabase Auth ondersteunt:** -- Email/Password (wat we vandaag gebruiken) -- OAuth (Google, GitHub, etc.) β€” volgende week -- Magic Links (passwordless login) -- Session management (Supabase beheert cookies automatisch) - -### @supabase/ssr vs @supabase/supabase-js - -**@supabase/supabase-js:** -- Browser-side client -- Voor onClick handlers, forms, interactie - -**@supabase/ssr:** -- Server-side client (SSR = Server-Side Rendering) -- Voor middleware, cookies, server actions -- Handelt sessions automatisch af - -**Waarom twee?** -- Browser kan niet veilig geheimen beheren -- Server kan veilig cookies zetten -- Supabase SSR packages zorgen dat beide veilig werken - -### Supabase Auth Functies - -**signUp(email, password)** β€” nieuwe account aanmaken -```typescript -const { data, error } = await supabase.auth.signUp({ - email: 'user@example.com', - password: 'secure-password' -}) -``` - -**signInWithPassword(email, password)** β€” inloggen -```typescript -const { data, error } = await supabase.auth.signInWithPassword({ - email: 'user@example.com', - password: 'secure-password' -}) -``` - -**signOut()** β€” uitloggen -```typescript -await supabase.auth.signOut() -``` - -**getUser()** β€” huidge user ophalen -```typescript -const { data: { user } } = await supabase.auth.getUser() -// user is null als niemand ingelogd, anders is het een User object -``` - -### Server vs Browser Client - -**Browser Client (createBrowserClient):** -- Voor 'use client' components -- Kan useState gebruiken -- Kan useRouter gebruiken -- Kan user events luisteren - -**Server Client (createServerClient):** -- Voor server components en middleware -- Leest/schrijft cookies -- Kan getUser() veilig aanroepen -- Geen access tot browser APIs - -### Middleware & Session Refresh - -**Wat doet middleware?** -- Draait op elke request naar je app -- Refreshed de Supabase session -- Zorgt dat user state altijd up-to-date is - -**Voorbeeld flow:** -1. User logt in op `/login` page -2. Cookie wordt gezet -3. Middleware ziet op volgende request: "Er is een session cookie!" -4. Middleware refreshed de session -5. App ziet dat user ingelogd is - -### Handige links - -Toon op slides: -- [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs) -- [Next.js Server Components docs](https://nextjs.org/docs/getting-started/react-essentials) - ---- - -## 11:00–11:30 | DEEL 2b: Zelf Doen β€” Auth Implementeren (30 min) - -Nu gaan studenten zelf auth bouwen in hun project. Dit is niet meer live coding β€” docent loopt rond en helpt. - -**Instructie voor studenten:** - -Volg deze stappen. Docent loopt rond als je vragen hebt. - -#### Stap 1: SSR Package Installeren (2 min) - -```bash -npm install @supabase/ssr -``` - -#### Stap 2: Server Client (3 min) - -Maak `lib/supabase-server.ts`: - -```typescript -import { cookies } from 'next/headers' -import { createServerClient } from '@supabase/ssr' - -export async function createClient() { - const cookieStore = await cookies() - - return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return cookieStore.getAll() - }, - setAll(cookiesToSet) { - try { - cookiesToSet.forEach(({ name, value, options }) => - cookieStore.set(name, value, options) - ) - } catch { - // Handle error - } - }, - }, - } - ) -} -``` - -**Wat is dit?** Dit is een helper zodat Supabase cookies kan beheren in Next.js. Copy-paste voor nu. - -#### Stap 3: Browser Client (1 min) - -Maak `lib/supabase-browser.ts`: - -```typescript -import { createBrowserClient } from '@supabase/ssr' - -export function createClient() { - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! - ) -} -``` - -**Wat is dit?** Dit gebruiken we in 'use client' components. - -#### Stap 4: Middleware (5 min) - -Maak `middleware.ts` in project root: - -```typescript -import { type NextRequest, NextResponse } from 'next/server' -import { createServerClient } from '@supabase/ssr' - -export async function middleware(request: NextRequest) { - let supabaseResponse = NextResponse.next({ - request, - }) - - const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - { - cookies: { - getAll() { - return request.cookies.getAll() - }, - setAll(cookiesToSet) { - cookiesToSet.forEach(({ name, value, options }) => { - supabaseResponse.cookies.set(name, value, options) - }) - }, - }, - } - ) - - // Refresh user session - await supabase.auth.getUser() - - return supabaseResponse -} - -export const config = { - matcher: [ - '/((?!_next/static|_next/image|favicon.ico|.*\\.svg|.*\\.png|.*\\.jpg|.*\\.jpeg).*)', - ], -} -``` - -**Wat is dit?** Dit draait op elke request en refreshed de session. Copy-paste, don't worry. - -#### Stap 5: Signup Page (5 min) - -Maak `app/auth/signup/page.tsx`: - -```typescript -'use client' - -import { useState } from 'react' -import { useRouter } from 'next/navigation' -import { createClient } from '@/lib/supabase-browser' - -export default function SignUpPage() { - const router = useRouter() - const supabase = createClient() - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - - const handleSignUp = async (e: React.FormEvent) => { - e.preventDefault() - setLoading(true) - setError('') - - const { error } = await supabase.auth.signUp({ - email, - password, - }) - - if (error) { - setError(error.message) - setLoading(false) - } else { - router.push('/auth/login') - } - } - - return ( -
-
-

Sign Up

- - {error &&
{error}
} - - setEmail(e.target.value)} - className="w-full px-4 py-2 border rounded mb-4" - required - /> - - setPassword(e.target.value)} - className="w-full px-4 py-2 border rounded mb-6" - required - /> - - -
-
- ) -} -``` - -**Belangrijk:** `'use client'` directive bovenaan β€” dit is een interactive component! - -#### Stap 6: Login Page (5 min) - -Maak `app/auth/login/page.tsx`: - -```typescript -'use client' - -import { useState } from 'react' -import { useRouter } from 'next/navigation' -import { createClient } from '@/lib/supabase-browser' -import Link from 'next/link' - -export default function LoginPage() { - const router = useRouter() - const supabase = createClient() - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault() - setLoading(true) - setError('') - - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }) - - if (error) { - setError(error.message) - setLoading(false) - } else { - router.push('/') - } - } - - return ( -
-
-

Login

- - {error &&
{error}
} - - setEmail(e.target.value)} - className="w-full px-4 py-2 border rounded mb-4" - required - /> - - setPassword(e.target.value)} - className="w-full px-4 py-2 border rounded mb-6" - required - /> - - - -

- Nog geen account? Sign up -

-
-
- ) -} -``` - -#### Stap 7: Logout Button (3 min) - -Maak `components/LogoutButton.tsx`: - -```typescript -'use client' - -import { useRouter } from 'next/navigation' -import { createClient } from '@/lib/supabase-browser' - -export default function LogoutButton() { - const router = useRouter() - const supabase = createClient() - - const handleLogout = async () => { - await supabase.auth.signOut() - router.refresh() - } - - return ( - - ) -} -``` - -**Belangrijk:** `router.refresh()` na logout zorgt dat page de nieuwe state ziet! - -#### Stap 8: Navbar met Auth State (3 min) - -Update `components/Navbar.tsx`: - -```typescript -import { createClient } from '@/lib/supabase-server' -import Link from 'next/link' -import LogoutButton from './LogoutButton' - -export default async function Navbar() { - const supabase = await createClient() - const { data: { user } } = await supabase.auth.getUser() - - return ( - - ) -} -``` - -**Logica:** -- Als `user` bestaat (ingelogd): toon email + Logout button -- Anders: toon Login + Sign Up buttons - -#### Stap 9: Layout updaten (2 min) - -Update `app/layout.tsx`: - -```typescript -import type { Metadata } from 'next' -import Navbar from '@/components/Navbar' -import './globals.css' - -export const metadata: Metadata = { - title: 'QuickPoll', - description: 'Vote on polls', -} - -export default function RootLayout({ - children, -}: { - children: React.ReactNode -}) { - return ( - - - - {children} - - - ) -} -``` - -Voeg gewoon `` toe. - -**Teacher Tip: Studenten vastlopen?** -- Na 5-10 minuten vastzitten: toon de referentie code op beamer -- Zeg: "Dit is complex, copy-paste is OK. Focus op begrijpen, niet op typen." -- Help met debuggen (console.log, errors lezen) - ---- - -## 11:30–11:45 | Vragen & Reflectie (15 min) - -Hier zijn veelvoorkomende vragen: - -### V: "Wat is het verschil tussen `createClient()` in server.ts en browser.ts?" -**A:** -- `server.ts`: kan cookies veilig beheren (server-side) -- `browser.ts`: kan UI events afhandelen (onClick, forms) -- Supabase kiest automatisch het juiste moment om te gebruiken - -### V: "Waarom twee environment variables bovenaan?" -**A:** -- `NEXT_PUBLIC_SUPABASE_URL`: URL is public, iedereen ziet het -- `NEXT_PUBLIC_SUPABASE_ANON_KEY`: anon key is public (maar kan geen private data lezen) -- Private keys (service role) zetten we NIET in .env.local, die gaan in server.ts als geheim - -### V: "Mijn login werkt niet, ik krijg error" -**A:** Check: -1. Klopt je email/password echt? -2. Is je account in Supabase Dashboard β†’ Authentication β†’ Users? -3. Is Email provider ingeschakeld? -4. Zit "Confirm email" uit? (check dashboard) - -### V: "Logout werkt niet, user staat nog ingelogd" -**A:** Vergeten `router.refresh()` na `signOut()`? - -### V: "Middleware error: 'createServerClient is not defined'" -**A:** Check je import: moet `import { createServerClient } from '@supabase/ssr'` zijn - -### V: "Kan ik als anonieme user stemmen?" -**A:** Ja! RLS policy staat op `FOR SELECT, UPDATE TO authenticated` β€” maar je Navbar toont Login/Signup want je bent nog niet ingelogd. Dat is OK. Volgende les doen we RLS policies correct. - ---- - -## 11:45–12:00 | Huiswerk & Afsluiting (15 min) - -### Huiswerk (voor Les 9): - -**Verplicht:** -1. **/create pagina bouwen** β€” studenten voegen nieuwe polls toe via een form - - Maak `app/create/page.tsx` (Server Component met form als Client Component) - - Form met: vraag + array van 2-3 opties - - `supabase.from('polls').insert()` en `supabase.from('options').insert()` - - Zorg dat je eigen `user_id` meestuurt - -2. **RLS INSERT policy** β€” alleen authenticated users mogen polls toevoegen - - Supabase Dashboard β†’ Authentication β†’ Policies - - Voeg policy toe: `INSERT` voor authenticated users - - `user_id = auth.uid()` - -3. **Optional extras (challenge):** - - Toon poll creator in PollItem - - Google OAuth inschakelen (zie Supabase docs) - - Edit/Delete buttons (alleen voor je eigen polls) - -### Afsluitingsboodschap: - -"Gefeliciteerd! Vandaag hebben jullie: -- Supabase gekoppeld aan Next.js -- Real data uit een database geladen -- Login/logout gebouwd -- Server & browser clients begrepen - -Volgende week voegen we RLS policies toe zodat iedereen alleen zijn eigen polls kan aanpassen. Dat is waar authenticatie Γ©cht nuttig wordt!" - ---- - -## Veelvoorkomende Problemen - -| Probleem | Oorzaak | Oplossing | -|----------|---------|-----------| -| `Error: Cannot find module '@supabase/supabase-js'` | Package niet geΓ―nstalleerd | `npm install @supabase/supabase-js` en dev server herstarten | -| Supabase returns leeg array | .env.local niet juist of dev server niet herstarten | Check .env.local, restart dev server (Ctrl+C + `npm run dev`) | -| TypeScript complains over `null assertion (!)` | Normale TS warning | Dit is OK, we vertellen TS dat env vars bestaan | -| `'use client' vergeten in signup/login page` | Component is interactief maar geen directive | Voeg `'use client'` bovenaan toe | -| Login page blank/geen content | Conflict met server components | Zorg ALL pages onder /auth zijn `'use client'` | -| Logout werkt niet, user nog ingelogd | `router.refresh()` niet aangeroepen | Voeg `await router.refresh()` toe na `signOut()` | -| Middleware error: "wrong params" | Onjuiste URL of key in middleware | Copy-paste van .env.local, check Format | -| "Invalid token" bij Supabase calls | Token verlopen of anon key fout | Restart dev server, check API credentials | -| User niet in Authentication β†’ Users | Signup failed, geen account aangemaakt | Check browser console op errors, probeer opnieuw met ander email | -| `router.refresh()` werkt niet in component | Router niet geΓ―mporteerd | `import { useRouter } from 'next/navigation'` (niet 'next/router'!) | -| Cors/network error | Supabase URL fout | Check dat URL eindigt op `.supabase.co` en https:// bevat | -| Password te kort / validation error | Supabase vereist min 6 chars | Zeg studenten: "Test met password123" | - ---- - -## Didactische Tips - -- **Pair Programming:** Zet snelle studenten samen met tragere β€” kennis spreidt zich uit -- **Show & Tell:** Toon je eigen werkend QuickPoll op beamer β€” studenten zien het doel -- **Error-driven Learning:** Zeg niet meteen het antwoord, vraag: "Wat zegt de error?" -- **Debug together:** Als iemand vastlopen, use browser console.log + devtools -- **Save time** β€” als >3 students dezelfde error hebben, stop even en toon op beamer -- **Celebrate wins** β€” als iemand eerste Signup working heeft, geef thumbs up! - ---- - -## Referentiematerialen voor Studenten - -- [Supabase Auth docs](https://supabase.com/docs/guides/auth/server-side/nextjs) -- [Next.js Server Components](https://nextjs.org/docs/getting-started/react-essentials) -- [Environment Variables in Next.js](https://nextjs.org/docs/basic-features/environment-variables) -- Alle code snippets uit deze docenttekst - ---- - -**Einde docenttekst Les 8** diff --git a/Les08-Supabase-Auth/Les08-Lesopdracht.pdf b/Les08-Supabase-Auth/Les08-Lesopdracht.pdf deleted file mode 100644 index eb83a68..0000000 --- a/Les08-Supabase-Auth/Les08-Lesopdracht.pdf +++ /dev/null @@ -1,219 +0,0 @@ -%PDF-1.4 -%“Œ‹ž ReportLab Generated PDF document (opensource) -1 0 obj -<< -/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 7 0 R ->> -endobj -2 0 obj -<< -/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font ->> -endobj -3 0 obj -<< -/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font ->> -endobj -4 0 obj -<< -/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -5 0 obj -<< -/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -6 0 obj -<< -/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font ->> -endobj -7 0 obj -<< -/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font ->> -endobj -8 0 obj -<< -/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -9 0 obj -<< -/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -10 0 obj -<< -/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -11 0 obj -<< -/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -12 0 obj -<< -/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -13 0 obj -<< -/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -14 0 obj -<< -/PageMode /UseNone /Pages 16 0 R /Type /Catalog ->> -endobj -15 0 obj -<< -/Author (\(anonymous\)) /CreationDate (D:20260331152247+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331152247+02'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False ->> -endobj -16 0 obj -<< -/Count 8 /Kids [ 4 0 R 5 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages ->> -endobj -17 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 699 ->> -stream -Gatm8b>R(K']&X:mLn5>=AMTGd@h(3)/+n_DR+6q2;%F,`rSB==*Epi),,f/E?%DgYKk$H1'JaQgAK5PJBoC/J#,CJKR\O[+sbTVeA&XA#*?AiuGZ>&T0eo1p]8\6f:hWsLc-T::dU/"pNbYHlBu+4O?g>L\R@`3GFo\qW7DC[Tq*.R\d:?YJa2#all,Lknm6<@ZIGE&a@@OL7Jd:R9RUXiKuFRS5(DS]Y;A0P8T(Me=[GR4(Je];%?P9iSUJ6"qC"^AlB^SneRYr@`.QN2kPJV"0TI[G'YtMf][!dnqF3;CfMP2`YQ_@jDK*?6qVDc>fg1lu"GIoU>koF6QlZsZI(oCL3hmQ_AHi_\j4CA%endstream -endobj -18 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 882 ->> -stream -GauJ!>u03/'F*Lmrebc"2D)km=0nsD!.iWC[V!.=\8ftbC[/jSG09USEtYbh]7'Q:RgTC&neF;+m%NRG!%rX6g6f*-AJ"6'eUP-/WA+J`KM,d]dir,YnSR%>E-+?c`3Rf8$j>Z7*-U2APGBnWU))AH9U@l7NN%hma`kC6!jB22.[^kBGWrmS`g#"V"NTM*,+2B_cm1%bOZeqOK^$>TlmQMRE+JUUek\<;!bZ$/l][M5Ze3*Ng"4%Xh*U\u]\S"sLt-Z9^roYuNmG\HHL[`s[I-R=Or,*N%AE@`d^2Q1`.;>bV#@SLn.e!^ctF'2qhf%<.]L2$$#c%o/02Ft$I&/PO$-tUrMg30NL1^SS&B7oiG1jC@tbG6uh.F!@G.BLlM0#2W2YF1#"FA&k<'b0+e4hA3)-!Q#+P>Ztf=0'K?=lYoSOuWb"I3roo>-e692$1BDkVb2GX-b*.&)G3fXLgo]'*sC9:f#*5p?:C=.#\=J4BAHDgT('gWdo*]&b8K>FgUp\NG&B]_%mkVsQ0iiA?[H`"eG!k%G7>2bH-]l@GX*eTH4j$LLhPdZ:4XP=;4S-(YBQu$l7()D:'[uGH.7)WT6d;`!M9<3+ldbJ?&d;_RY7f/^:>@VK!^4f4g>;G;jbrktWUZ7RmB+GdD5'>>m/\bO7pUoVa*n>F`k^HKE8WV3OY2blE!ZEM4amnVf>aYEljM\hL~>endstream -endobj -19 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1216 ->> -stream -Gau0CmrR2B&H0m]Z0`pm3J?MO-BjijEnjBm1sHcaC$0NS.%jBa`cbq0-.D,YIOj*%"1U3?#7LYbC#;+f[qdB/pn[b71']cLU&-nTC-m.Qe0[),"6bNBcRi>GFA:a!AjF1R4@0-/^iamiR',],%M6fBXM=rO=R#orf"sRa,tp;=&13c*KR+_*q_uf1TKO,E13UQU%ej\'nX(]>=9=ie6'AkX"b_(2^/T(N_WO*RQf6/MfH)c8"?O8CftKh$>R%^9i,JZna$#Lf$Na%SV+F4`qe>@I2\g9St\[[rRI4jAjaYYscM>GPNt5D8'tdJ;_N`HXAR3]fPFp;`qPK@)W'a?&o/6<@=*AO=6r8M?>Z?ien`YchK,r4a\:5$9H131+AS;FWMID+)?-iU9M%TO399tif\0RYS%<]Xec!Vs8$\bp^S#'AS?r)>^9]E'K'DHWpP,sM*G8g"?IpkTo5/h'W!t:i3C]:F,C1oFlTBpr"G_7RRV/U]n3uSr8UaO,`E0Pako?;\,hr!G]j8oC=Z]BjDN\H@L"57HLuD=tF2INR(Tf`.5MPP2[Ds/RoJLBu_*0An5MH7Y+*=AUpVdTBM-m_BTRYXTIf89%_5Wnf0at8"T:(8DSo>rFfnH3<`s`p>,+E%*%;]R$Zi]=OCc'R+GDmd^U2H2hq85Bendstream -endobj -20 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 967 ->> -stream -Gb"/%gMWKG&:N^lVL?=aMcjouVlcsiBc-d(BIu)]3`YkL10Q<-s0s%GTgnaQ/.$$]$T@9cW6?R,n6(M/GWk^k0%?8rf[irZ\6FLGFfBr@Nc?B'+hX/1/kiCfP\PBtkZ[/5NajR`2n>(e:SrTZlB^KfV3L1fG\_atce4ntJn-qtUd55+5HL@Z-4KH'hf\FK.%`.r;ppqk8jVVH%Vb&1hl-d6.$0mQh;If'&;\GcL&&%mcD0/n?KjIA!/q#E11ciZs48f7*mi8&\kKj*CR?*5sjc\3n$_AAlQ't:LOU`@G-V[]_F%oQ7?QH*kLZW#A<"IM#2I=aV>KC9k`/b`B$^"gY"&1(B[#ecW"SD.bb_`G:U"BQAhE]\+g^8W("(b:<`H5)./.7-`,CWWE,qOAj$]]l*&L'Tu$-D`rCF&"Y@Ln[O,e"V#H@_/@,"h%4)q)LC\A.L!n?VPUj?,l9R'l8lP_c';0qRTePILQC;?5[=+kW%kIYTSS*eL?X-[,($,%J%)bk?/!NfWl3A#ZV=V^D9#[NKPXW3D302IdN[UU~>endstream -endobj -21 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1211 ->> -stream -Gau`SD3*C1&H9tY(nL(^[Lu0%pKY>\?17V:mBq9mf.Wsu9e_Zl32pDRmB-0P*BT?/]G+Dip&r7<4e?q2T(.%J@,m%+h]rGOecY4A!TH@_"N3)ZkZu!q?KZ*Z/0tD=$t:S2&-g&uE=h.jgbt"K'*=Yu(jAe(C!<70*)/CX!c9,Z#)r;>Q9`XoqMl6+X!21&*AaX]\7,N&+tj>L\3,E#([+"[JH!C2/l6Gm#-)7f;jN9DltI8mZG.J>)#omh.N)op)R2J7d'C"m3`B)2bIs0!")(_PU&OF5CnJ(D)9%a*`iqJ;HeC-%R!6b6Yf"P7bn4I1Tc9u7(\qZra1u[^&.G-ndY?LU&5<@Kl]AUf+,WUH*XN-30P9m]aO&NBN'rfG0A8kTG:Q;k(.O;7oB#OQ4fAYAC/DrX99sL1BYmeQ/"$L^/lFlM.H0Vs+qd)qF&3rjfD=uuNT_pW5c\fcJ^KE(1+g"2_7n;]K:?(dFXHGrP(HaJ8fa+:TQ?RE-:gbTj5\Vp->.oth`+C/8&LO:(oCArA@/And%$Z9!]'5)Am#=Lf4us&]t`qZE(ujNYU$O+FfmM;'jPne75fgHYS3f%QN,"jTXT#I'Bc:'Ehc?u3A$A&6VT7CQHhTU$&SBbZG%2Tp?c*02:LLc-A%6&Da%B?_>:QBs1J+U&\Fr*:!leACSqqUrU7g/Bm7shS-K8I5O4pn:0&PiX:]Z.h(!%pNZYuQ?5/m;is+dkgF_@\NPLBSM6LkDRZ.(6p`@9si>S/N8J9pgM>3RgkuJ1sj,MgH:%XDj92Y&aQbWZd1PmbG+#8uL0kA6_:#N$"";I\+=f(gn4rP^\$(b&.e:9T@V?VMFsRkSJKN#fZHed-T]!CR]JaL)s+PpEs/=p$,OVdG3Xp.@sn%@7q04F3"^YYXpP$dnhn'1As+0hM3P-9G_eG\W+ot[,nr6_Cl9rH/mgg2_endstream -endobj -22 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1369 ->> -stream -Gau`RD0+E#&H9tYf_,tRD&L#2foo19j*!P)>USH)gL8)H`#;UN4*#P43$/+om[XKt(i6%V>@9FPp3*'Vp:4J^d%\!d^69PpW!E2agL*+i]*@#r4Qr?P8[%Zp@@*:2@g.u.g!KHIQ5p,@mA&F%'0'jT#KmQ\DWH<_\bZJ=e0(CW8sip[+WPGp#lT/Uc$+s]3![g$:#eMj&NV^*L,>N7Qm5NXOR5=%0Bg*s?p<"c'-Pn0"_>++Nt(F@3ZtU+6oj!KBAW^#9N?flp]:B4ll+aN>4M!gq5*+i1^HElBi#nEZXCue%)?fci.3&/(eTuoV49ig=b9RP#B&J.rW*p]1LI$gL8fb:%]PTu@2-`jjp:Ja49WXnL>rOWkh@1lBem6W=_>K3g,hQR'=*=#X;qbg/=iGqn23q[MenhB-S#B&8#]HSlN6:6ZAajOgDeFlhYn6u11Q9G8;0&qaJu$n89(R.NHLQG0HDaYCUAHZQM+^(?mM7<;NqPK$@!$nV:=*Fb5mV\"KMa[G%A!#2qY,>hldGPY:2meF(CZc$;6Zl0%LfqK@OBR*FE2HDetc/o`\cc5sG%YkqI@t@iNN_sM)o:0>CP;,n*/B-:IMi*a)9&t8#phjc!I`pIuMjCh/ppViMX5YR)MhJh)[,EB8*f,4n"]6Y4/nV4TlVSa.!>S)8jhprkl'I.d_n9DH6)u#PYeC``#(*'CGbK&0ZjirQ$&#'4-LNg9E#0^^XDXVE[RR9;?V]11iU&BBu)V]e\P[N#V\(gr/cL-#c"W8MTfELaA![W;U=X#OX&Y7;4\61b[p!i,ob^-Jq-Pf,_6:XIUB_o-I5X"P^^t^KJn!;p//0K_ha'm)D(Nq7UupEHuYLH@48V]]^>X\gLRGT?b1*r=@n0c@8!lH@PM+9*AHKj+'NQ6Ij2`U3Np#Z7:f5h*6Si(C:#i,0O(7]H6O9W\:C(=Hp=t%[+Yendstream -endobj -23 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1160 ->> -stream -GauI5Df=Ag&B7`8>V].CAW#RV+_fkk;j,TKjO]:Op$pL&NBgpKO7nF;=%%`/gDo?NQ;8aH(9[G*=TegRSTk[i\qaL^2*=M>VN.](!4pdoH[fK]TBVH6gF;3-uSiL[OcbHd%G3BVR?RTK`\8.:q0K*Jd=YKXO_`h.YmaXCRn1WSoYOOu(@p;CiQLB5,4=%:j=fJc&")PPlJ:"8S+D]e%\%6YEkhJ%(\s2?E$rS_eX*4K%hBIGk2!9kf9W"0J/,3B1+e6cTMkD)*01I*!Yk0@L=-2&-Y!U64uU=*YdDOmY%$+h$/G('j!mu/F_T>b.[\j+IEEpYLr>)92a!,e5FBX$a^etF]1p*)+7*XUmu=aRmR>3*WXC&1Rj,tAf%REfPUbNKIa!?>mfY70WgB$XQfLd\C+:(&#Jq[q_QX+lW*jaOR"k,nUnX18^endstream -endobj -24 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 661 ->> -stream -Gat%^a_oie'LhcqMS!sK<&*J`nhN\_(,-*F(:#@$U#d)[Ys^&]D"R0h;B?J$78XA[W&L5n*uTD(AFWu*^dF-o-O,k\IfUK6"6;7#k+/sk4%>7YA<`o._g".9=>nRge+#GOf"Mf>;^qpc!o+MF7PWejqHbr7u[)s"3D56ZQZrk&H_^Q3:6id*lY'(GT,Kk5gh%0Jm1blUbZHnO?&bGS7oUC8,%f+en-ZWiSn'L$[LB1Nt4613Y7X0OS73@TB%c=Ot=,a4Dmh.P.]N$UL[[jT)mpofj9KE'K7AE,E,ZN\'k?TcX*!%Nk5&/#@Po+a3;]i+]q[YPjlA!$rZ4_[Si<@b9$T%p7#@S$Vc>/oOGa=8h7:Z]&U1O?)OmO^P(^ZUUN([G:p(7j@Z]0cJ~>endstream -endobj -xref -0 25 -0000000000 65535 f -0000000061 00000 n -0000000122 00000 n -0000000229 00000 n -0000000341 00000 n -0000000546 00000 n -0000000751 00000 n -0000000856 00000 n -0000000971 00000 n -0000001176 00000 n -0000001381 00000 n -0000001587 00000 n -0000001793 00000 n -0000001999 00000 n -0000002205 00000 n -0000002275 00000 n -0000002556 00000 n -0000002662 00000 n -0000003452 00000 n -0000004425 00000 n -0000005733 00000 n -0000006791 00000 n -0000008094 00000 n -0000009555 00000 n -0000010807 00000 n -trailer -<< -/ID -[<71e7c7d830850d86ed44e0355ffd582a><71e7c7d830850d86ed44e0355ffd582a>] -% ReportLab generated PDF document -- digest (opensource) - -/Info 15 0 R -/Root 14 0 R -/Size 25 ->> -startxref -11559 -%%EOF diff --git a/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md b/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md deleted file mode 100644 index 8c5159c..0000000 --- a/Les08-Supabase-Auth/Les08-Live-Coding-Guide.md +++ /dev/null @@ -1,2472 +0,0 @@ -# Les 8 β€” Live Coding Guide -## Supabase Γ— Next.js + Auth - -> **Jouw spiekbriefje.** Dit bestand staat op je privΓ©scherm. Op de beamer draait Cursor. -> Volg stap voor stap. Typ exact wat hier staat. Leg uit met de "Vertel:" blokken. - ---- - -## VOOR DE LES BEGINT - -**Zorg dat je dit hebt klaargemaakt:** -- [ ] Het poll-demo project geopend in Cursor -- [ ] feature/les-8 branch checked out (VOOR de nieuwe commits) -- [ ] De code hieronder klopt met wat je ziet (BEFORE-state) -- [ ] Supabase project aangemaakt en database klaar (polls + options tabellen) -- [ ] Twee demo polls in de database -- [ ] Slides geopend (voor Deel 2) - ---- - -# DEEL 1: LIVE CODING β€” Supabase Koppelen (09:10–10:15) - -**Doel:** De poll-app offline maken en live verbinden met Supabase. Alle data gaat via het Supabase API. - ---- - -## STAP 1: Installeer @supabase/supabase-js - -```bash -npm install @supabase/supabase-js -``` - -**Vertel:** -"We gaan de Supabase client library installeren. Dit is de JavaScript SDK waarmee we rechtstreeks kunnen communiceren met Supabase β€” zonder een eigen backend API te hoeven schrijven." - ---- - -## STAP 2: Maak .env.local aan - -Bestand: `.env.local` (in de root van je project) - -``` -NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -``` - -**Vertel:** -"Deze environment variables bevatten je Supabase project URL en de anonieme API key. Ze beginnen beide met NEXT_PUBLIC_, dus ze worden in de browser zichtbaar β€” maar dat is okΓ© want dit zijn public credentials. Je private key bewaar je nooit in de client code." - -**Waar vind je deze?** -- Ga naar je Supabase project dashboard -- Settings β†’ API -- Kopieer Project URL en anon key - ---- - -## STAP 3: Maak lib/supabase.ts aan - -Bestand: `lib/supabase.ts` - -```typescript -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey); -``` - -**Vertel:** -"Dit is onze Supabase client β€” één plek waar we de verbinding opzetten. We gebruiken `createClient()` om één client aan te maken die we overal importeren. De `!` betekent 'deze mag nooit undefined zijn'." - ---- - -## STAP 4: Update types/index.ts - -**BEFORE:** -```typescript -export interface Poll { - id: string; - question: string; - options: string[]; - votes: number[]; -} -``` - -**AFTER:** -```typescript -export interface Poll { - id: string; - question: string; - created_at: string; - options: Option[]; -} - -export interface Option { - id: string; - poll_id: string; - text: string; - votes: number; -} -``` - -**Vertel:** -"De database structuur is anders dan wat we hadden. Nu hebben we twee aparte tabellen: `polls` en `options`. Elke option is een eigen record met een `poll_id` referentie. Dit is genormaliseerde data β€” beter voor grote apps. De votes tellen we per option, niet meer als een array." - ---- - -## STAP 5: Rewrite lib/data.ts - -**BEFORE:** -```typescript -import { Poll } from "@/types"; - -let polls: Poll[] = [ - { id: "1", question: "Ik ben een vraag", options: ["optie 1", "optie 2", "optie 3", "optie 4"], votes: [1,1,1,1] }, - { id: "2", question: "Ik ben een vraag 2", options: ["optie 1", "optie 2", "optie 3", "optie 4"], votes: [1,1,1,1] } -] - -export function getPolls(): Poll[] { return polls } -export function getPollById(id: string): Poll | undefined { return polls.find((poll) => poll.id === id) } -export function votePoll(id: string, optionIndex: number) { - const poll = polls.find((p) => p.id === id) - if (!poll) return undefined; - if (optionIndex < 0 || optionIndex >= poll.options.length) return undefined; - poll.votes[optionIndex]++; - return poll; -} -``` - -**AFTER:** -```typescript -import { supabase } from "./supabase"; -import { Poll, Option } from "@/types"; - -export async function getPolls(): Promise { - const { data: polls, error } = await supabase - .from("polls") - .select("*, options(*)") - .order("created_at", { ascending: false }); - - if (error) { - console.error("Error fetching polls:", error); - return []; - } - - return polls || []; -} - -export async function getPollById(id: string): Promise { - const { data: poll, error } = await supabase - .from("polls") - .select("*, options(*)") - .eq("id", id) - .single(); - - if (error) { - console.error("Error fetching poll:", error); - return null; - } - - return poll; -} - -export async function votePoll(pollId: string, optionId: string): Promise