From d5599a601cd285ce79d0c66c06fab03cbff5effd Mon Sep 17 00:00:00 2001 From: Tim Rijkse Date: Tue, 7 Apr 2026 16:58:45 +0200 Subject: [PATCH] fix: add les 8 --- Les08-Docenttekst.md | 207 ------- Les08-Lesopdracht.pdf | 346 ----------- Les08-Slide-Overzicht.md | 186 ------ Les08-Supabase+Nextjs/Les08-Docenttekst.md | 575 ------------------ Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf | 238 -------- .../Les08-Live-Coding-Guide.md | 551 ----------------- .../Les08-Slide-Overzicht.md | 169 ----- Les08-Supabase+Nextjs/Les08-Slides.pptx | Bin 146524 -> 0 bytes .../Les08-Docenttekst.md | 324 ++++++++++ .../Les08-Lesopdracht.pdf | 346 +++++++++++ .../Les08-Slide-Overzicht.md | 145 +++++ .../Les08-Slides.pptx | Bin 146188 -> 147013 bytes 12 files changed, 815 insertions(+), 2272 deletions(-) delete mode 100644 Les08-Docenttekst.md delete mode 100644 Les08-Lesopdracht.pdf delete mode 100644 Les08-Slide-Overzicht.md delete mode 100644 Les08-Supabase+Nextjs/Les08-Docenttekst.md delete mode 100644 Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf delete mode 100644 Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md delete mode 100644 Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md delete mode 100644 Les08-Supabase+Nextjs/Les08-Slides.pptx create mode 100644 Les08-Van-InMemory-naar-Supabase/Les08-Docenttekst.md create mode 100644 Les08-Van-InMemory-naar-Supabase/Les08-Lesopdracht.pdf create mode 100644 Les08-Van-InMemory-naar-Supabase/Les08-Slide-Overzicht.md rename Les08-Slides.pptx => Les08-Van-InMemory-naar-Supabase/Les08-Slides.pptx (89%) diff --git a/Les08-Docenttekst.md b/Les08-Docenttekst.md deleted file mode 100644 index ead0f6f..0000000 --- a/Les08-Docenttekst.md +++ /dev/null @@ -1,207 +0,0 @@ -# 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 | -| **Lesmateriaal** | Lesopdracht PDF (studenten werken hier zelfstandig doorheen) | - -## 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 Server Components en Client Components -5. Een formulier bouwen dat data INSERT in Supabase - ---- - -## Aanpak - -Studenten krijgen een **Lesopdracht PDF** met alle component-code (volledige UI). Ze hoeven alleen de **Supabase queries** zelf te schrijven (gemarkeerd als TODO-blokken). De docent legt concepten uit met slides, doet een korte demo, en loopt daarna rond. - ---- - -## Lesplanning - -### 09:00–09:15 | Welkom & Uitleg aanpak (15 min) -πŸ“Œ Slide 1, 2, 3 - -**Wat te zeggen:** -- "Vorige week: werkende polling app met in-memory data." -- "Vandaag koppelen we Supabase: onze database-as-a-service." -- "Jullie werken vandaag **zelfstandig** met een PDF. Alle UI-code staat erin. Jullie schrijven de Supabase queries." -- "Ik leg eerst de concepten uit, dan gaan jullie aan de slag." - -**Check:** -- Iedereen heeft Supabase account met polls en options tabellen -- Iedereen heeft QuickPoll project lokaal draaiend -- Deel de Lesopdracht PDF uit (digitaal) - ---- - -### 09:15–09:45 | Uitleg concepten (30 min) -πŸ“Œ Slide 4, 5, 6 - -#### 09:15 | Slide 4: Van Array naar Database - -**Zeg:** -"Tot nu toe stond jullie data in een array. Dat werkt, maar is weg zodra je de server herstart. Supabase geeft ons een echte PostgreSQL database." - -Toon het verschil: -``` -// OUD: in-memory -const polls = [{ question: "...", votes: [0, 0] }] - -// NIEUW: Supabase -supabase.from("polls").select("*, options(*)") -``` - -#### 09:25 | Slide 5: Supabase queries - -**Toon de vier belangrijkste operaties:** -1. `.from("polls").select("*, options(*)")` β†’ Haal alles op met relaties -2. `.eq("id", 5).single()` β†’ Filter op 1 record -3. `.insert({ question })` β†’ Nieuw record toevoegen -4. `.rpc("vote_option", { option_id })` β†’ Database functie aanroepen - -**Demo:** Open Supabase dashboard, toon Table Editor met polls en options tabel. Laat de relatie zien (foreign key). - -#### 09:35 | Slide 6: Server vs Client - -**Zeg:** -"Belangrijk patroon: Server Components zijn `async` β€” die halen data op. Client Components hebben `'use client'` β€” die zijn interactief (forms, klikken). In de PDF zien jullie dit terug." - ---- - -### 09:45–10:15 | Deel 1: Setup + Queries (30 min, zelfstandig) -πŸ“Œ Slide 5 (blijft staan als referentie) - -**Zeg:** -"Open de Lesopdracht PDF. Werk Deel 1, 2 en 3 door. Dat is de setup, queries schrijven, en componenten kopiΓ«ren. Na de pauze doen we Deel 4: de /create pagina." - -**Studenten doen nu:** -- Deel 1 (PDF): npm install, .env, supabase client, types -- Deel 2 (PDF): lib/data.ts β€” TODO blokken invullen (getPolls, getPollById, votePoll) -- Deel 3 (PDF): Componenten kopiΓ«ren (page.tsx, PollItem, VoteForm, poll/[id]) - -**Jij loopt rond. Veelvoorkomende issues:** - -| Probleem | Oplossing | -|----------|-----------| -| npm install failed | Check internet, node_modules verwijderen en opnieuw | -| Env vars undefined | NEXT_PUBLIC_ prefix? Dev server herstart? | -| getPolls() returns [] | Query syntax checken. Staat er data in Supabase? | -| TypeScript errors | Import vergeten? Types kloppen met database? | -| "RLS policy violation" | RLS uitschakelen of SELECT policy toevoegen | - -**Check-in (10:00):** -"Wie heeft de homepage al werkend met Supabase data? Steek je hand op." -β†’ Als minder dan de helft: kort voordoen op beamer. -β†’ Als meer dan de helft: doorgaan, help de rest individueel. - ---- - -### 10:15–10:30 | PAUZE (15 min) -πŸ“Œ Slide 7 - ---- - -### 10:30–10:45 | Uitleg INSERT + /create (15 min) -πŸ“Œ Slide 8 - -**Zeg:** -"Nu gaan jullie een /create pagina bouwen. Het formulier staat al in de PDF β€” jullie schrijven alleen de INSERT logica." - -**Toon op beamer:** -```typescript -// 1. Insert poll -const { data: poll } = await supabase - .from("polls") - .insert({ question: "Mijn vraag" }) - .select() - .single(); - -// 2. Insert options met poll.id -await supabase.from("options").insert([ - { poll_id: poll.id, text: "Optie A", votes: 0 }, - { poll_id: poll.id, text: "Optie B", votes: 0 }, -]); -``` - -**Zeg:** -- "Eerst insert je de poll β†’ je krijgt het id terug" -- "Dan insert je de options met dat poll_id" -- "En dan redirect je naar de homepage" - -**RLS policy:** -"Voordat het werkt: voeg INSERT policies toe. Staat in Deel 4, Stap 4.1 van de PDF." - ---- - -### 10:45–11:30 | Deel 2: /create pagina (45 min, zelfstandig) - -**Studenten doen nu:** -- Deel 4 (PDF): RLS policy toevoegen, handleSubmit implementeren -- Testen: poll aanmaken β†’ verschijnt op homepage - -**Jij loopt rond. Veelvoorkomende issues:** - -| Probleem | Oplossing | -|----------|-----------| -| "RLS policy violation" bij INSERT | SQL policy uitgevoerd in dashboard? | -| poll is undefined na insert | .select().single() vergeten? | -| Opties worden niet opgeslagen | poll.id doorgeven aan options insert? | -| Form refresht de pagina | e.preventDefault() in handleSubmit? | -| Redirect werkt niet | useRouter van "next/navigation"? | - -**Check-in (11:15):** -"Wie heeft succesvol een poll aangemaakt? Open Supabase dashboard en toon dat ie erin staat." -β†’ Toon op beamer als demo. - ---- - -### 11:30–11:45 | Vragen & Reflectie (15 min) - -**Mogelijke vragen:** - -**V: Waarom async/await?** -A: Supabase is over het netwerk. We moeten wachten op antwoord. - -**V: Wat is het verschil tussen Server en Client Component?** -A: Server = async, data fetching, geen interactiviteit. Client = 'use client', useState, onClick. - -**V: Kan ik realtime updates zien?** -A: Later! Supabase heeft realtime subscriptions. - ---- - -### 11:45–12:00 | Huiswerk & Afsluiting (15 min) -πŸ“Œ Slide 9, 10 - -**Huiswerk:** -1. /create pagina afmaken (als niet klaar) -2. Validatie: vraag niet leeg, min 2 opties, foutmeldingen -3. Extra: delete functionaliteit, styling - -**Slide 10: Afsluiting** -"Volgende les: Supabase Auth. Inloggen, registreren, en bepalen wie wat mag. Tot dan!" - ---- - -## Tips voor docenten - -1. **Niet te veel voordoen.** De PDF is self-contained. Studenten leren meer door zelf te doen. -2. **Loop ronde, spot problemen vroeg.** De eerste 10 minuten na "ga aan de slag" zijn cruciaal. -3. **Check-ins doen.** Vraag om handopsteken. Als <50% het heeft: kort voordoen. -4. **Toon Supabase dashboard.** "Zie je? De data staat echt in de database!" -5. **Authenticatie is volgende les.** Zeg het af en toe, zodat ze weten dat RLS nog tijdelijk is. diff --git a/Les08-Lesopdracht.pdf b/Les08-Lesopdracht.pdf deleted file mode 100644 index 04d3e89..0000000 --- a/Les08-Lesopdracht.pdf +++ /dev/null @@ -1,346 +0,0 @@ -%PDF-1.4 -%“Œ‹ž ReportLab Generated PDF document (opensource) -1 0 obj -<< -/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 6 0 R /F5 8 0 R /F6 19 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 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -5 0 obj -<< -/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font ->> -endobj -6 0 obj -<< -/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font ->> -endobj -7 0 obj -<< -/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -8 0 obj -<< -/BaseFont /Courier-Bold /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font ->> -endobj -9 0 obj -<< -/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -10 0 obj -<< -/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -11 0 obj -<< -/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -12 0 obj -<< -/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -13 0 obj -<< -/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -14 0 obj -<< -/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -15 0 obj -<< -/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -16 0 obj -<< -/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -17 0 obj -<< -/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -18 0 obj -<< -/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -19 0 obj -<< -/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font ->> -endobj -20 0 obj -<< -/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -21 0 obj -<< -/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -22 0 obj -<< -/PageMode /UseNone /Pages 24 0 R /Type /Catalog ->> -endobj -23 0 obj -<< -/Author (\(anonymous\)) /CreationDate (D:20260331170259+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260331170259+02'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False ->> -endobj -24 0 obj -<< -/Count 14 /Kids [ 4 0 R 7 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R - 17 0 R 18 0 R 20 0 R 21 0 R ] /Type /Pages ->> -endobj -25 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 952 ->> -stream -Gatm:9iKe#&A@sBCi?#<#YVI>WA?]aThlS0Rc;a'(LrB]`3C?'T``'D/(lXP#/c#QbAqd6]C5.9-59^4;Yq=b!5=2,GX?s:#k*:i`.RO\^4jk\Trk6=Lp*2_L%5KaJ\Z]Zj@';A&m;kcN6lBiR6hF!E%W7?@_#n=;Z:K6"[AU7YG`WCp@7/(!$R[/rMKaUA2Ij&8Z^.Jq-bYo0DUeVI1o%D?0U8%u7lBK;U(#J_C$7+=!D9^kB6Q3[l=bb1>$%\0OcCO%a*dD859?t\P27fU$M0/\p7/=3n$*:K#>+H;6?ONVJ.+=%1TcoE-.,G:C:Cr)c.K&pCt#*liE7)N5Ecu9=W6(q-<)A#CAh8D!#M=l#]qkI,E#6n7Ea+4FtU8NT\'6/Ak5*@*>R;l+#'=3W8.9SiK1g!t'4T^J>joi1H3C2W_gX\FPIjY%R/X\?^j>oaQ/T0mL!B^q5SWH$oeO)8fuuA2,P&2cpLg1LN?/*-Oe4[h>Qe1W24()Q)9uX!pp`IC;7$ZYq62SYA_qfXL-r045$/P%pBCkud++ISi:hbG]2rZOIF>Dj+1g*UcR'_ZFsukPUOhm[f^=U":[0$TS.eIO$@u~>endstream -endobj -26 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1267 ->> -stream -Gau0CD0+Dj&H9tYf^ojA*6.Og2OW?+d\FKLF%KP+HT/tS,E*7/Cs/(I8__@:^Qj`a&1G7tJf2aAYG>fTT1:CD0_$@`*[Um-4_VPU5V2,)!HAMQHDf-Se\9PSgsWM;0_gbTD9L*fjd36cfDS2Q:ScHe[i*_FuL?!#.k8B"i_E]l=6n+*=8n!\+q_4#HRgU:XO9n*ZT`h`lEPsksA@hn4cX+WT#:d9ghL9iR1L.Gl)1cG/HjA5,oHApCYPsRN)Er,2XP"V@"S![(S;/X2<9HZlW[)P$R<@Jll)<=K8R+SG>dL;_:_H&t]:-+:9:Jl9JAlM0?(paQHF+%s5csrpgr-[GgG0!M[K@1^c;eiDf^2C/sB#Hg-nbc$?;B7/[(!fAY"Qp-"QGB$S(!,dRh@#J#0pftp&es=@'J=X97-#Enk,e/eDfB]AI!Y?qd(R(O3^2hP#+$E-&'%R[H+Vb"nEFe"m^Q-"h?^ZPJI99V+cajncN`i"Ii[hR2!&H3L4*ot,:4m.ceai\;A>scV)5VR$L47XFJP(T:8ns8;5h^)CFQ80*tL@H,@)I'P[?Q-CXG%I!*20re6ZH;GbA&MeL5WIeOSL53=QQSREE(nn]oYWIEEf6V>W3sF_m?aD6$_fhP_,Pkt0_G!(d\l;4h\@SoV9hbT,Z'VW@$'hN$X"C5!b#IX?=-*GsR4fffU>Bf=,Jb%H0P>Bii\p=aL(*_"nG>-'7C_-Mdq$P<_)@K1;jPU_j/SSAJ]mUb_a5cH!L5`@H)='TgMj;;rr-QPnp[>b*ulcAr%FBlN6.`ESRcrVI@5)snsr/H0KAeA.-Rg1d.HiVL*n\dcXA5"_[7(]*cG?+6HNbPlhXq+endstream -endobj -27 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1525 ->> -stream -Gb!;d?$#!`'Re<2\BP]/CkO*adn\XhdrrL'RjLg98nZ2s(JnRW,0SZF8cF(?CdW^^$"UBd1h8:B=45VZ8&n\MYis',pNR*cgkGH?*4UOe!4]odke;gZ]6dV+]SZ7`o0O%0n6M[VcjQfp2)F1thf45#JRsT<=D40g:r>F7P:oVY*Rb](6oB[DIh/Xp`"YTE:*L]E)F[$W$/rdI3JmT0JT+V`P#r\hpB%p]-Dr59^iE@p%E.X1<$,SRUPmBbDX&<8=OP/"ua17X*Uf90&(qc+M:j3oCffE?0:2tUDemt>N1QF,`k=t_6,hThN[>kcYN^A":F)Zd6u(pqTqa/a3a^3Q(XcHTe(L%k^JJ3+fanF.R>hJsTW_S)2b"7%Q4R7]U@]&W4McopmY?u:cqj8V9?^e6;qqO2-F<"n9elN_K8&"hZ\7_J-cY`>!CTTmR[jS^f;kI-sWY\4([L!C./7:\<@/>#SdY2-1Blk$QrUZf+3#ISAT7o9U*Ym;#l!;U/XN9Ja"iCX7=\?__m(&hE'*u36UL-_6H>36\R@aK[)U-f\u/\:UBI5/urs&4FH&T40E`:4WmU*P-<$ToBl-9W-SJV"OJe.B28FQcmk-cCWSE,2R`r>!/[Etgt/H]Cl4+NLm'$t`@/DHNUZDHGT%"6[).!n&,ro%#7X(+tMQ7'b26,,R)*(8iJ3p]*cp7M,jJ!dZGPBr!'K%bP7c;V0MUdm[p6enh60Q)bD*<*&'/ZK`seiY:ZUA^F8`fV9kT(lQZj_AFHOhTd;ekLd1c+o4q-GI&A,G![d;:GBGU+IVT2U+&[+%1,Np0T`92HAus/a`<_7B#><53T#Tj&c4#78",aAYUEF"On:\)C@#M,nIl]12#AKLaV/k7js+."`Q[L+f4_VA^%7b0*#hMp1X=&C2b;""rl=/B%mdl:hE:$erL=pp;7;2QE&W#%mg7Qsin7[K4_HcMN5NjiS$URN)ORaL+"Fl[69tYje)Bq%`JU,&A0qNOO#>YP.Q<@cHo0,5irJMU038?Tg+`2U?"d5U+DJP/8ajMK\DYTmTtgendstream -endobj -28 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1260 ->> -stream -Gatm:D0+E#&H9tYf\oIk@L5d8+d(4;,Yab'4cW`&rDNLNO;:YDl'0Z=i*lEL\N=,o5V3oW_s-9+pN^q#?O1c7Qg"Gl!Ve@#p2:(i%AP?`a)YR0%A-+LD[j#.l.VHhgHL1;YAY$1oqe1+D:u)JGC!Y>]h[1g_=G5'"+8[]3m!MP;'J`e?u$G@ru-i'&S(#7i$1lMQ=;[&oY$DY';`*`(^$4&']+WYhqTXn0T&:]=#OsQQ5:F!D!i%'Dd9VI7Fg_C]tclD#9-/o7EL'h+?&0)ji1nA&EErcZg6VK*,P@Y=MhdJHG)C!L]go*Y/ZtdtQCp"JSP8dcYgMa%:gYN*a\=0k(C#95c6.[;7[d?9*/8Jm?L6foI0K/?\59N/nBA@eO^$GnNcSPbl6eYW.;H;R'_Z/tt%gToM]N;V%G'b7WtP1$:M:2VS7&=f\#Fd8l*6L#XF2cu_>iQnt/UK0n*cH1Xq^Kh9L?qb*G`?J`LmKZ.E\YmW\C4['?5"tL&43dWfpCM:I$a)PnM>$hn9)2;dTW@rI#QAfsG0&r&FDYWq&iLd'sWqY+q6s_X$3%%5"O)^s6ocB%Zcl)C>?HWNQ'?]nb^"@D7(U)_S*Tq5l-N4C!nXDj?Q@Yq3VVIi9f6(0a-.C-0d)s=;L3^2U7WMbq;cuT`IJT89/3i#mendstream -endobj -29 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 791 ->> -stream -Gb!#[gMWKG&:N^l7TM%mQ53Rr*(]BpMU_@=Wg!k`aUcCq(n-.)bMnGAs1O%<Ap9]1+DXZprHJc,8$GB*4;n)5T34u=SY%.0135k8m2>mU=RdTmo9?2cSPZ'.9imbU->j)Y-"5C]a$uWZ1/^B<[!eFKKM<>'o2W-R-p:M/)6!PZ%Rc$Z5*Xp[^'-E(Z_ms5HCSb8$`Z1X2FZ%XA0%["gH.9N$4":lZH*(RCICZl?8m]E&6a(V(VAZ6WK"!K4T#\'S&<+14W+c/2A'#$W.INb@($!9k3YJV!nNTmZi,-s",@Qq]C(_^F)jVpEqQ`WLOG\-peCi:$[m7Pqk`'f*u\g3Q!,SK0-L]<82)Mn3u%.;MZWW#1_A)e'!!FaYK)7?8\%Za$*0/B+%6E9+)B:9JQ@/P4i>Lk6+F`.17:j/s=\`M8bFq.tP)T;+oo9bnZ`1(Q_I\9Z^91=;9"]/fi8_e@0A4(gke#R1U6V&gq,/ZI><='Pl+F#qm1\T9#lT3D-$/F&:<2MPRCn`94dUSluN0jD7Xm0Mb"M?bmT&@A$Zia09m=P5Hse:7dc`[L"b[>dRnNPs&kIMtC9*Cu#>o/542#k#hW.XgIFUO4q#Wp-$sD^2#UkS:#aEH@\k"g0:KONj%jW?\raOBqShQS2fella_I.IGA.R)i])qTDH%,JbBJt6)]/~>endstream -endobj -30 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1301 ->> -stream -Gau0CgN):C&:N^l3dt5:#m,sQ?pcF*Fe!L&-Z2IKC('%s)#1hIAX!N])Cu!Lb+,EM(n[H,0LVMDbOlFd0m=nF;]NW3pHn!J"8_*H#tE3>hu`]bQ[NF'p0qe0X@BN$L[3j4=7eC%.seDJkZTPt>UeeTC6n_)XL<#oCX?sf+:rET-AM8hcu*qsSI1%<#,2Ei\oBnH+6!RB8b1h-6;0+,*99tAKS24E"=9@%fYCSlF1;h'*-q-i.pe%orniV4>5,^-h.;`iQ2`Y)?GLc\jXS2^7l1tD'#g4u#^IIUMBIjc?"cnf+^]ghD:Ho(5<\JfhX'RJf*$th@i"qs9)sR.>I(XoAEmROhZmOj:IB*b"URtR^g)prIY%jeQh:;6%(7dr[5%7t[&]fOkY[@_'.jQBHU5\D4]B@++-d12^nMQfW#MQ)*o&sKGWnG[Zj,bf^RXGq^D-$N9oA]cTjt=$fPrU&'W*1OekhTNNifZTDbQ*FIHTU2JPG.cn)UeMIU?BA\LU^f9"eKD3!HZ$sd/&)4WV?CNi=KB-Bi)+TXS0]&^X&h7KH4_+lkRQ0H$ba-*SK(b_`\D=6UIf=`a/H9cP%%Ng4c.e!]>'"Vg]BI[2ZTupa;03b:s4;A\idO-=i5o!$rN;kJ;'3C?l4jAg2j(H[hLX@_Q;68k-FJmB#5@t>Wr;:kBQUi;4or%=Od:]04f!M5roCW9ooMmE*<8\SC:2oKW0JRL#O_4HTRB6p=^@OTOu]$*(dL8P+$nif)$KTma6e-%^PoF5.>V%%qD3<(F)o2WhCrAVLYW8/2:UEa*o(f-EUJggm#_t;dBT,Fnm;!RXVjlV=6FDY9phru_ZBXB#YkWBm3o&$5>po[>?DIPW;Hm`ERh/~>endstream -endobj -31 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 675 ->> -stream -GatU0?#Q2d'Rf.GSG%e=_I,n_ABQH6>,FpB['"(<]K!_6XFM`Z9<*mL4p+NFUhSF0!cbES?g%:>LVcq"D83Cc(^D`B'\#T+#OR^=JfB(DZj2RSHb)+TIG:Y:3'O%Ml`3!V-D'[/NfQR'j/YWR=D>]tX\5jY6Wl%K*/<(XI;_A5'aB$CU?!.uBX]ItJjd'rD25-56.R6m;-4i`c&Xjj[1jPU6C&!%\:BS\JmVaPKS#I?X&'>3d2OZoL8\XNj9sJ9>Mh>"m,@3tC-U^fc?n`+a+lEA1o$\b;6f=;=_%Ll"(YkM=7f__m1*AJp_K_ei(kUkTe+r>8DQFXlES,C($I]I5*jg*,#hM+7<,"d;oQEmOe`\K_=?Q&I3g;o5TNKoVpaOUQ"8qFiucb"^'mn8/;=A3pS8\ULF]hU9u\MU0-l:mFaT[i+L^04rJ"L!\3gi4FW0Br-VC-c^U5&U#AQ@g//WEI9#OUC6dn`JN2endstream -endobj -32 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1259 ->> -stream -Gau0BD/\E'&H9tYR.:PYabDYWCoUN.Enh[P)LC>AC.hPYG[_GIc)oTkXY*Eq6n!hO7&=?tCY.pC/t9,)0M"iF8rdBS;?NA0bOuVd#+Ki%;cl@pj\[cVm[85*U\Ggbf7\a-=\h`_BUU>85CJ];biL[j`k)6a@;$KTV>R'=^4K7O>.<&8I1198!_CfhS-J&P47hY_g7OJQ3V&&0b;95JbJ>U(^X^5h+&=9l8lk&srla+iV)2(4&ta8F[eLH2`#2!of[\9a>R5,?VA;`_gnQ*]-Ja4tp-cL^XJreeGHa0EbjOR4jYp%+f'=WED^9@cl7Olib3PrkFqVNY!3>t^P?MjWt_JVYH+R(`7%QCNL"SVYJD!t%cJ0^^C]nXUX;.?NHT3Ga4E[q;F:h3g;$p\3"+Ui&]b*^>l#;[jXM&q\PeML?\f$4E.a&G$cpnXB%H%&Q`4;]i%oQR5IV4T7p*.g;i40[7\XgP1&[V#:I)l\YX)efoP-58PU38AfMGY^HU.sO@q?8,t(Rb`toL=(uh&$^D%le+@Hl`A1,Rdq6#Phk#K!OCA#RqY_&f):eO]'/q..K5asX,GAmHqD8nIBendstream -endobj -33 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1341 ->> -stream -Gau0BD0+Gi%0#[%Ja,n!?:L?g,YRR_&kQ7-OISX+kW\,VuspT:m\[Gr!4[ff!cE#hXhiJLJCC6k>O]75=@MZ9kXbR-u?(rhEjoI>hsEBAgT0@lN03.E7W-@]%)WO\,+WC\G\cm9Eu)fg0uZWLd1P/P[$>p@=sdB%'jQ2<\C@h)+M%?SI(4dEg?2>fZ:7?fT7G%(/LM"M=h1_g>[#]5=Ou$O:iXLP;*$X4+3T'QC;s`)T>ohVAAY"0khWRn&nMmbRuhAO[j&(MOA2W!"Xn.q0IYiX[SL7YfeA5`&OP'uDR$ZPJ!!OSa,12JoL;qqmeW"\ad=;k$PjPk%oN]B^cmF/F8Ib1Q]3H/>rmfXr"18'P$29?O<;$n5i[HTGl2nX/1r\r`+Sm&;??C]OKDSo^&&Xteh!?+%Bem,LB-GA2!KVQhf654H0L3_n#oW,Brrq_Pql4XY>keU?1lG'7Jt48/*f9YI[eRD!`U%Va75HU,0Ff0KtrEI/]Z1nFH(X0rJTZZg&1pn0"-"F8QSe&A>JIe`g"jF#U=2;<"p&'Uh@;/%L?2W,Q:/Z(U3H6Y"lNA6OVg3?5Dr.LQ:d`2j`&5&mj]LM5H9+:/-FtR*YSK!0C#F)b_R]qn?'9T_/EHb,<5~>endstream -endobj -34 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 466 ->> -stream -GasbU?V;n(&B3Q$;]L1"7FNO,?[WU#^=3C1*u?2;>$'G&hVHZg(Vfj[!s-DQbIufj8nT]cqTHlD6nX-lSVem8ctM];5]-H'P(=tkqV27,38jttjO0IL(5NNuqjV)_[4Xt81rm"+^E,E,L6m#fBmf:bVH6AP4&&g)1EL4HnE]%I!Su(pU2iDE-&'I.;j7@[DI6ZU,%#Y'`0Y$`~>endstream -endobj -35 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1122 ->> -stream -Gb"/(>u0HF'Rf.GgmF'ef9T/\V3o4,pFFoS\h7OR!tQ/.5]te[dj\[\s*\P6.MS"6D1RHO`o@'d4nn9t$R3s9"3&$%*l)J/.c1d[*SPp,L2'!N%)0/CG9rKfe'qC(Tf3P4c'jM.Aj]fN?<318%LcA&WN)Rj`Hu.0PO=QX"L8hX-XJ)kqU^*)F)=:Pd)fc]p+/_Hch`G-Z)Tib4;0(FA,_Uang>aQSmJRUX8bKDjl<^NPLc+;[-J.)V'SC75R-O^BN'3V8E&(R6WBO)kU7"1s>Rq`GNJCZC"eSJiVo(R+Z(lOqp`Kt-2105mUgM"SU%NZ38LXAMi+SV9;P[Kct)ZP&&"fSm@i&96Y&6)k??`J//=P6a+FG7b=q9DohKB,+8CZY7g=0\@gaZ'7((;A*+<0u$Dq'dQarZN93'*/Af6.`D&POW>i2F=EEs9hJt.E+*Y9K,6SYu'C[SU?:kotu,M=oRWYTE?iOd/GP7B(Qjo)G?QRM*JJc5m*#dh;`J\DE.7NWB[c9`7l8cu%2U$C;sVq`^Ka!5OlgJ6n\/=0(N^#F]LI,CI6":CHt(PSqdn,rs('XGsudF*8UG]^*?FK?f(ZI-m,@2B.9.Yn/Hk4g,kVK#k]8%ZVi$QIq:?I=--P$>l-6uA^BbUSueFJd[[e:A-e<.74Gf3?I+!Gg'Yn2/&hp'5sN-_TZ"'usN%(%H1[3V?;c:C^S[U+?R--#W0PMWkUcl!fd#FHfeVXbp-pQ@8^3e$NDSQ\D+=,WgJ7=k>J9-JR3q;Haf11lPAJXG\Oq^3HOmY#q5`UdhM,Z~>endstream -endobj -36 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 545 ->> -stream -GauHJd;GF-'Rf-pNd,;"iF3]RUs15tg@eE]lbm:>9Q6q3YVRuT\n_7/#KG>j;Pe,?1?'-eT6m_hY%.ms1toWE'sL5Kp7?j?8CsM51.?MKGH(2YTtb50!Jus^Ta5@'BG)V'SJ`bo')UhuNq(9tLP4;?,`2GWG`$"4$V/'lpWkZ+8Sj\9LjdbmMc8fY0H3UUO3/RR%:*.[$EoH?F2[qtCCobM2=q4+"d#>8";@Slf#jN7V2u[fOJ_K$t&8+P)2miQ=*%CE[u*cp&n4g9CaAsL`oXe1N+AL5[c&#EaGOG.h\5B?MD>IS1#XS>R,mnCC+'q/Xm"fTu.htac:qa)3!]GN`LiV[;&_cB+]H_EZ)bmk6")n6:T6PP1b(GiRnPrVg`AB!uHF=e?N>.G,2CK4FhU3WWAO(L!lQ[aeegeH+"%W4smo*\3Kj(&~>endstream -endobj -37 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1622 ->> -stream -GauHKD/\/e&H88.EQtDg6PR4;;?s:-;tjkpg*HBB!DQGmi`,+DWVbq(.W2V?`;qE'7l=,70-cXDQ^`9\qsehDHJr*IgXcaOSODp%hR%Aqlr+E3W<81f)]q%0(0")!TMs$22u?f>IX`.]2N2%$E,E9gPBl.@TTf$eLaSECIB2hZkd:]S*^i3suJ!cCkOj9p4O172PUCP5F!R!P4&%HkipQ[-!IPp^dHQUR%;HLu3R8u?>fIJbPR-=u(77\4Fj3!QPHa/36<)$N]f_t34l:R-8nSN_9]@Z5lPd[d6F!L5'QuF?j0QGQ2fXOd\W>KG0@qKi?_0JT14>CoXDi@g>;8%gB8m@t_S`\KO*5ghD'-5-:u!,5U1K>CeeJflGmjZ#&i>g9L#5V);Z5fup_,X+r\S6613\T&a(S&V(nu.QL6V"K,M?[$:fsI?krlVB>TXp()GcMkZ[uhp4bl9AdYG4502o3Qj0QI4`_g89n/]\/u?_ODUibCJi^Cg$[&J\i:`T;Z"Z.PffD/P\PBFTjI1_FV<_N$X:3aU!+]+e//X?XqfiTs.4YQq!7&&h>(^)"mLK4JI].cTV.T1AB"8LPnc/Pab0f.&I]p9\+0DC%'j5?d^b@/of'37(pgG;0=:u"36$;+U,'p!iD+\KcEEZ4"q"8E$;D\Y+HW4$5-kB,TIDbO;9I@jJ!nZ[SsR["UD^r1rpbdD>2:R\(YaR1S]pQAhdlX;lgtlb:3G-u$9lBGa3%+aeln:7D^SN*E.8Q?.?S9%-ZnC=lAF1q[:IQ`>9_;6$n%cuBaJ90n3Pa%o&;>U``1:G_L=nf:6*jPou3UoO85:WDQRGQt@T55\QsgATf)<--(JoXd:BJl%F>nF6&7c&pu$D)54gpK*qi-8?cC^WNi6F;ThPjFkj$/,GL$.@/K9i*JYAAkOl&I`hh,l/(n;TL9>0qtLaX7f@`S:B5B-6*'q86>~>endstream -endobj -38 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1632 ->> -stream -Gatm;gN)%,&:O:SD#G""j3T&r&Xg,jini*Y'5O*Sc#d>*Q6Ck\%74*Z:B/!8hF,ML.4I,EU1!>Z4bqYnQ6*P\H0+cO"*cnYb:=)c!g>_8![iN?8,+buGs(V3bD8,T64sgKiRm[`-p^(fG">$@5mWJm]7*)^8UUC?H@CI5?73rB_[lrA)8^iPK[J+=?EBNC3.o_<-00,kAW5Hp@,s=E7U._f?-Y:%L-\St>Z.5=)1>nu[V@Kd5Xb\]#*LbE%t[[g.pA0[k"NMBJ1Y%Wa6g@fE2+kQ6\_3gZs[\[htTO@6bi!f4-r();ue?J?45fp.S![]^(:sS,i#Aqad>W0SAQpcJ,Yo_S'dd1iIQ-`&X6OpSst0[#\P_rUH+O/*0R=%fD,dJlFOmQD3l;snNPB8.pnM..;\cZ3nqa;E@Jp-G^kI.4ZZiW>^=?L^J,1b?1glX`h0Bu+-"a(t9Hi9G0?Jgp8@Tn)8cA4ghEc7n'$P`$9Cko/43!rV:-4>*%>;X,.0@>08sCGmu9/M122NZV/:Phon=E#Wc0E4duV68EEf*k,ZOZXdU--+d"UWtc_3@)Za&Nn4,X>akO^+dYO?dH(EdS+FL1VX8n*\7IZ8G[KDnYbK-Z8[f(5n8NUA:LY_+_5MldN3GA[O8TllpAN4Z;G3t0YS=HJ*M]W(qh)c2B0)\s>h>ChB7$kdXZn>%_u$oc=,O+&Q,g,t`BJ]JLAb,;pn>YI;hiuFl1YKH1]&pqTci;!0=C-Y,QnZF^'G:Le$`*^P5W1gAYcADup$J,JhEt@7PY8@5AYm=N"tf;3+)OV@`A:"@5Oo?Y_gP]H:5_mCP^^N(Dl-95CH[5r8P?]\qj8$7q!W~>endstream -endobj -xref -0 39 -0000000000 65535 f -0000000061 00000 n -0000000143 00000 n -0000000250 00000 n -0000000362 00000 n -0000000567 00000 n -0000000672 00000 n -0000000787 00000 n -0000000992 00000 n -0000001102 00000 n -0000001307 00000 n -0000001513 00000 n -0000001719 00000 n -0000001925 00000 n -0000002131 00000 n -0000002337 00000 n -0000002543 00000 n -0000002749 00000 n -0000002955 00000 n -0000003161 00000 n -0000003245 00000 n -0000003451 00000 n -0000003657 00000 n -0000003727 00000 n -0000004008 00000 n -0000004161 00000 n -0000005204 00000 n -0000006563 00000 n -0000008180 00000 n -0000009532 00000 n -0000010414 00000 n -0000011807 00000 n -0000012573 00000 n -0000013924 00000 n -0000015357 00000 n -0000015914 00000 n -0000017128 00000 n -0000017764 00000 n -0000019478 00000 n -trailer -<< -/ID -[] -% ReportLab generated PDF document -- digest (opensource) - -/Info 23 0 R -/Root 22 0 R -/Size 39 ->> -startxref -21202 -%%EOF diff --git a/Les08-Slide-Overzicht.md b/Les08-Slide-Overzicht.md deleted file mode 100644 index 8e8ed08..0000000 --- a/Les08-Slide-Overzicht.md +++ /dev/null @@ -1,186 +0,0 @@ -# Les 8 β€” Slide-overzicht -## Van In-Memory naar Supabase (10 slides) - ---- - -### Slide 1: Titelslide -**Titel:** Les 8 β€” Van In-Memory naar Supabase -**Ondertitel:** Koppelen van Supabase aan Next.js -**Visual:** Supabase + Next.js logo's, BLUE achtergrond - ---- - -### 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:** -```javascript -// OUD -const polls = [ - { question: "...", options: [...], votes: [...] } -]; -``` - ---- - -### Slide 3: Planning vandaag -**Titel:** Planning β€” Les 8 (3 uur) - -**Timeline:** -- 09:00-09:15 | Welkom & Uitleg aanpak (15 min) -- 09:15-09:45 | **Uitleg concepten** (30 min) -- 09:45-10:15 | **Zelfstandig: Setup + Queries** (30 min) -- 10:15-10:30 | Pauze (15 min) -- 10:30-10:45 | **Uitleg INSERT queries** (15 min) -- 10:45-11:30 | **Zelfstandig: /create pagina** (45 min) -- 11:30-11:45 | Vragen & Reflectie (15 min) -- 11:45-12:00 | Huiswerk & Afsluiting (15 min) - -**Extra tekst:** "Jullie werken met de Lesopdracht PDF. Alle UI staat erin β€” jullie schrijven de queries!" - ---- - -### 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: Supabase Queries -**Titel:** Supabase Queries β€” Vier operaties - -**Vier blokken:** - -1. **SELECT alles** (met relaties) -```typescript -supabase.from("polls") - .select("*, options(*)") -``` - -2. **SELECT één** (filter + single) -```typescript -supabase.from("polls") - .select("*, options(*)") - .eq("id", 5).single() -``` - -3. **INSERT** (nieuw record) -```typescript -supabase.from("polls") - .insert({ question: "..." }) - .select().single() -``` - -4. **RPC** (database functie) -```typescript -supabase.rpc("vote_option", - { option_id: 42 }) -``` - ---- - -### 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:** Setup + queries klaar? Na de pauze: /create pagina bouwen! - ---- - -### Slide 8: Zelf Doen β€” /create pagina -**Titel:** Zelf Doen β€” /create pagina - -**Ondertitel:** Het formulier staat in de PDF. Jij schrijft de INSERT logica! - -**INSERT voorbeeld:** -```typescript -// 1. Insert poll β†’ krijg id terug -const { data: poll } = await supabase - .from("polls") - .insert({ question }) - .select().single(); - -// 2. Insert options met poll.id -await supabase.from("options").insert([ - { poll_id: poll.id, text: "...", votes: 0 } -]); -``` - -**Stappen:** -1. RLS INSERT policy toevoegen (Stap 4.1 in PDF) -2. handleSubmit invullen (TODO blok in PDF) -3. Testen: poll aanmaken β†’ homepage checken - ---- - -### Slide 9: Huiswerk -**Titel:** Huiswerk - -**Verplicht:** -- /create pagina afmaken (als niet klaar) -- Validatie toevoegen (vraag niet leeg, min 2 opties) - -**Extra:** -- Delete functionaliteit -- Styling verbeteren - ---- - -### Slide 10: Afsluiting -**Titel:** Tot volgende week! - -**Tekst:** -- "Volgende les: Supabase Auth" -- "Inloggen, registreren" -- "Bepalen wie wat mag doen" diff --git a/Les08-Supabase+Nextjs/Les08-Docenttekst.md b/Les08-Supabase+Nextjs/Les08-Docenttekst.md deleted file mode 100644 index 7744e37..0000000 --- a/Les08-Supabase+Nextjs/Les08-Docenttekst.md +++ /dev/null @@ -1,575 +0,0 @@ -# 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 deleted file mode 100644 index 839d14e..0000000 --- a/Les08-Supabase+Nextjs/Les08-Lesopdracht.pdf +++ /dev/null @@ -1,238 +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 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 deleted file mode 100644 index f75439e..0000000 --- a/Les08-Supabase+Nextjs/Les08-Live-Coding-Guide.md +++ /dev/null @@ -1,551 +0,0 @@ -# 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 deleted file mode 100644 index 8010a02..0000000 --- a/Les08-Supabase+Nextjs/Les08-Slide-Overzicht.md +++ /dev/null @@ -1,169 +0,0 @@ -# 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 deleted file mode 100644 index 203339f2207ed1e26fa7fdbf4eba7e671fcbe22a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146524 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 link werkt straks na Deel 4." + +#### Stap 3.2 β€” components/PollItem.tsx +- "Dit is een Client Component β€” zie `'use client'` bovenaan." +- "Het berekent percentages en toont bars. Copy-paste." + +#### Stap 3.3 β€” components/VoteForm.tsx +- "Nog een Client Component. Hier wordt `votePoll()` aangeroepen β€” de functie die jullie net geschreven hebben!" +- "Copy-paste." + +#### Stap 3.4 β€” app/poll/[id]/page.tsx +- "De detailpagina. Weer een Server Component met `await getPollById()`." +- "Copy-paste." + +**Test samen:** +- "Open http://localhost:3000 β€” zien jullie polls?" +- "Klik op een poll β€” kun je stemmen?" +- "Check in Supabase dashboard: stijgt het aantal votes?" + +**Troubleshooting als het niet werkt:** +- Lege pagina β†’ RLS SELECT policy mist +- `Cannot find module` β†’ check import paths +- Stemmen werkt niet β†’ vote_option RPC functie mist + +--- + +### 10:15–10:30 | Pauze (15 min) +πŸ“Œ Slide 7 + +**Zeg voor de pauze:** "Na de pauze gaan jullie zelfstandig Deel 4 doen: de /create pagina. De volledige UI staat in de PDF β€” jullie schrijven alleen de INSERT query." + +--- + +### 10:30–10:45 | Uitleg INSERT + start Deel 4 (15 min) +πŸ“Œ Slide 8 + PDF Deel 4 intro + +**Doel:** INSERT concept uitleggen voordat ze zelfstandig aan de slag gaan. + +**Wat te zeggen:** +- "Het formulier staat compleet in de PDF (Stap 4.3). Jullie hoeven alleen de handleSubmit functie in te vullen." +- "Maar eerst: Stap 4.1 β€” de RLS policy. Zonder INSERT policy blokkeert Supabase je." + +**Loop door Stap 4.1 (RLS policy):** +- "Open Supabase dashboard β†’ SQL Editor" +- "Voer de twee CREATE POLICY statements uit de PDF uit" +- "Dit is tijdelijk β€” volgende les beperken we dit met Auth" + +**Leg Stap 4.2 (INSERT theorie) uit:** +- "Er zijn twee INSERT stappen:" + 1. Insert de poll: `.from("polls").insert({ question }).select().single()` β†’ je krijgt de poll met id terug + 2. Insert de options: `.from("options").insert(options.map(...))` β†’ gebruik het `poll.id` van stap 1 +- Toon op whiteboard: + ``` + 1. INSERT poll β†’ { id: 5, question: "..." } + 2. INSERT options β†’ [{ poll_id: 5, text: "A" }, { poll_id: 5, text: "B" }] + 3. router.push("/") β†’ terug naar homepage + ``` + +**Zeg:** "Nu zijn jullie aan de beurt. Stap 4.3 in de PDF: copy-paste het hele bestand, en vul het TODO-blok in. Ik loop rond." + +--- + +### 10:45–11:30 | ZELFSTANDIG: PDF Deel 4 β€” /create pagina (45 min) +πŸ“Œ Studenten werken met PDF Stap 4.3 + +**Wat ze doen:** +1. `app/create/page.tsx` aanmaken met de code uit de PDF (Stap 4.3) +2. handleSubmit TODO-blok invullen (de INSERT logica) +3. Testen: poll aanmaken β†’ verschijnt op homepage + +**Jouw rol β€” loop rond en help:** + +Typische problemen: +- RLS INSERT policy vergeten β†’ "new row violates row-level security" β†’ Stap 4.1 niet gedaan +- `options` niet gekoppeld aan poll β†’ `poll_id` vergeten in de insert +- `router.push` werkt niet β†’ check `import { useRouter } from "next/navigation"` (niet `"next/router"`) +- Form submit herlaadt pagina β†’ `e.preventDefault()` check +- `poll` is undefined na insert β†’ `.select().single()` vergeten + +**Check-in na 15 min:** +- "Wie heeft de RLS policy al toegevoegd?" +- "Wie kan al een poll aanmaken?" + +**Check-in na 30 min:** +- "Wie heeft een werkende /create pagina?" +- Help studenten die vastlopen + +**Snelle studenten:** +- Validatie toevoegen (vraag niet leeg, min 2 opties) +- Styling verbeteren met Tailwind +- Delete functionaliteit bouwen + +--- + +### 11:30–12:00 | Vragen + Huiswerk (30 min) +πŸ“Œ Slide 9, 10 + +**Vragen beantwoorden:** +- Open ronde: waar liepen jullie tegenaan? +- Concepten herhalen die onduidelijk waren +- Eventueel: laat een werkende /create pagina zien van een student + +**Huiswerk bespreken (Slide 9):** + +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 + +**Vooruitblik (Slide 10):** +- "Volgende week: Supabase Auth" +- "Inloggen, registreren, en bepalen wie wat mag doen" + +--- + +## Troubleshooting Overzicht + +| Probleem | Oorzaak | Oplossing | +|----------|---------|-----------| +| `NEXT_PUBLIC_SUPABASE_URL is undefined` | .env.local niet geladen | Dev server herstarten | +| Lege pagina, geen polls | RLS policy mist | SELECT policy toevoegen in Supabase | +| "new row violates row-level security" | INSERT policy mist | INSERT policy toevoegen | +| `Cannot find module '@/types'` | Import path fout | Check tsconfig.json paths | +| `PGRST202 Could not find the function public.vote_option` | vote_option SQL functie mist | Stap 1.5 uitvoeren in SQL Editor | +| Options verschijnen niet bij poll | Foreign key mismatch | Check poll_id in options tabel | +| Stemmen werkt niet | RPC functie mist | vote_option function aanmaken in SQL editor | +| Form submit herlaadt pagina | preventDefault mist | `e.preventDefault()` in handleSubmit | +| poll is undefined na insert | .select().single() mist | Toevoegen aan insert query | + +--- + +## Voorbereiding Checklist + +- [ ] Eigen QuickPoll project werkt lokaal +- [ ] Supabase project met polls + options tabellen +- [ ] vote_option RPC functie aangemaakt +- [ ] SELECT RLS policies staan aan +- [ ] Lesopdracht PDF gedeeld met studenten (digitaal) +- [ ] Whiteboard/marker beschikbaar voor queries diff --git a/Les08-Van-InMemory-naar-Supabase/Les08-Lesopdracht.pdf b/Les08-Van-InMemory-naar-Supabase/Les08-Lesopdracht.pdf new file mode 100644 index 0000000..cdf831e --- /dev/null +++ b/Les08-Van-InMemory-naar-Supabase/Les08-Lesopdracht.pdf @@ -0,0 +1,346 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 6 0 R /F5 9 0 R /F6 19 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 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +6 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +7 0 obj +<< +/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/BaseFont /Courier-Bold /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +10 0 obj +<< +/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +14 0 obj +<< +/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +15 0 obj +<< +/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +16 0 obj +<< +/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +17 0 obj +<< +/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +18 0 obj +<< +/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +19 0 obj +<< +/BaseFont /ZapfDingbats /Name /F6 /Subtype /Type1 /Type /Font +>> +endobj +20 0 obj +<< +/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +21 0 obj +<< +/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 24 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +22 0 obj +<< +/PageMode /UseNone /Pages 24 0 R /Type /Catalog +>> +endobj +23 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20260407143958+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260407143958+02'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +24 0 obj +<< +/Count 14 /Kids [ 4 0 R 7 0 R 8 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R + 17 0 R 18 0 R 20 0 R 21 0 R ] /Type /Pages +>> +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 952 +>> +stream +Gatm:9iKe#&A@sBCi?#<#YVI>WA?]aThlS0Rc;a'(LrB]`3C?'T``'D/(lXP#/c#QbAqd6]C5.9-59^4;Yq=b!5=2,GX?s:#k*:i`.RO\^4jk\Trk6=Lp*2_L%5KaJ\Z]Zj@';A&m;kcN6lBiR6hF!E%W7?@_#n=;Z:K6"[AU7YG`WCp@7/(!$R[/rMKaUA2Ij&8Z^.Jq-bYo0DUeVI1o%D?0U8%u7lBK;U(#J_C$7+=!D9^kB6Q3[l=bb1>$%\0OcCO%a*dD859?t\P27fU$M0/\p7/=3n$*:K#>+H;6?ONVJ.+=%1TcoE-.,G:C:Cr)c.K&pCt#*liE7)N5Ecu9=W6(q-<)A#CAh8D!#M=l#]qkI,E#6n7Ea+4FtU8NT\'6/Ak5*@*>R;l+#'=3W8.9SiK1g!t'4T^J>joi1H3C2W_gX\FPIjY%R/X\?^j>oaQ/T0mL!B^q5SWH$oeO)8fuuA2,P&2cpLg1LN?/*-Oe4[h>Qe1W24()Q)9uX!pp`IC;7$ZYq62SYA_qfXL-r045$/P%pBCkud++ISi:hbG]2rZOIF>Dj+1g*UcR'_ZFsukPUOhm[f^=U":[0$TS.eIO$@u~>endstream +endobj +26 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1229 +>> +stream +Gau0BD0+\p&H9tYf\@ip;iKfsCbk63\`VU>BjM:tQRNqE(aTH(aX7IZ]:8`m8hpm0K>Y54?n;gpXe+?sSmR?E0$)^bJ@CCBs,h'9W?16^Pks#5rk.;o0Y'&k4u19_Ju-Tf7PZ>E.`JI5g,)4`dOkCbuElV1A],CkWqHqRR0h.063Zj"E\8o*o`^V`Z#'.GJN_U33#Bf_!ql(b=naJ69l0G;MGjX%)scDf/"=@YBZ3q-MW84&sV4B/"ACFgD)F=p+"'9d`$q5otrF?bD5t`'a8b2oO*)I0ed9oqWERH'rl,DZ6fjI^,>(S%Gi[gG9JfJ,83/^;AH>`!;r'7dbcTo)O%g#Qqjk(VHZl@I=7,F8Rt*r&!1Yt'O:VNa(#F=92gCUK!>OX-T$D^L_Y>`gBN6S?[9M&!r*".XZ$)s"D5htoncJK)B2chB)-f),Fc%jU7=NW152k(IE]5K6P[S"PD0fSXg"Di7$EcH@Zq!IG:A$[75c5KXPWA18<<61Tcg0F;K&;:OYZIgcWW;f0:8/G#)V)sl-TrVAj:o0D(4QKV'b[cg"h<",NS6VUhE.L=("7GF71VG$7XjgD:lQrS%K%@Y9jVFkBMBYfo;H9'GcD03U-g,cV4"EJ*bGU_>fDUuR4`PR^DDY,Lgn@R74Dmn=&G&T.bJiQ]@sn`K%,2F]ag_75<3ROq@i_=9=BrVBE,7CpZ2WA5fGDHb[N*?Y4Kh7H;d+"TSI&!&1ScO'>?GH[Zdmo*7uBXkL_2j3'/,?9T(BJ)0*VWo_n@2$gKF1'38jqLHOWqC/)*l_Hiu):_1KLfWRo!k*"t#EJmYsKNiG"\B'GD5UMV>"B-`h.X)=D"U"]$;_a5WD!L>fAH),5)BVC]&gZ+OIgi_!R*"OqKnq;EQ7A%3*gr"!`SM-i~>endstream +endobj +27 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 746 +>> +stream +Gasal;,<]I&:VB$n^ZCsg31X@a$G]%@^J>K`9QmBR$\%Hh",F"?-orQs*GQB'0X$2Q7uinB(h^4k2UFJinZOt!f%(GIH06,P&'Q;"\;C.S;cl*]_;gt(<0\)0b%U3j"Ro]p=KT?a':_rZ36f,_-K+YVISDhrqqK#"e;nFC;Oh_'IfEL]OIWHf'6C)NCg_YL?VmcL#q3MU6F::r/kcEr?iEp>TW$-?pB43-;&qJ5*)&Oi#/;I=l4M+r^aS:SA)MHelZe8`kKB11.7A,uZ:<%6(NR*QMY(j3%*%=E5aj0@lfk[X@$tlWL2+*W)'&;B4npquLE!P#gBnJ\=RH+G4Wh4RkH.o(J@f+Ur9ei=gYNsuDgIq#>uJ'CL8hSj2%[XMWtad\f^BJ7AuuGHAXC]fPP4El^n7;Yr9[]IJ8s5Q7-A3^EiXol.>1nWk6:"]1f#`9MdW;^(.=.BQWV1kf#UEcK:]QY=!0_u%8a\rN~>endstream +endobj +28 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1400 +>> +stream +Gb!SlD,91]&H88.1$+Tba2[FhVc?_og>968*X>QW1tB]f-HUjLL\7*O"T8#>Y&0=u65"UH6j\a7h-&(Jo?9<ZC<*R$7m.'hX&=+nRF]J#!2C8?\qCipKE8CS#gQ?#s@71c]Rs!95ujo=nSm"KYr\Qm?X-@/T;_lSDQgL>XEjXXP,e=`rN@2#R#-T5%t.k2Xm%;`7r<0f?nYChKWF#,;Qo;W+6AJj#(,W'Q\gR@3cNqYuF:Yg*'dhYCs7Ee(^4nhb`/Tq#gtBO\Z:.TW`^I2a[a;4sIF76K'/b`eJ@$h[1=.$n,+M19U9]S-4Q=9dC%Z0b4fo@"f,fYn2#"2-!lV<&PfCPUqb_OLb('f""H1:T$`8tqXl!H+7=[^>a<"1V7&[WmXg"4@/M(m9hNV`ISb(!-L;(pY4/9A+rWSaN=7]J`Ma#e2&i9;:/Kl/N3[71D\k23RMqmhXU0JU*mf*sBXCnJ;k/RmNfdUSZ[L.%G!4oeT=/"RZh_D8/ub=f,P-s1>D%1G%b+R,iVX%endstream +endobj +29 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 285 +>> +stream +GapWf9hWDY&A@g>]]89#G1&9+-UC]C]ne]:-+:/6V+n74K$:]KuNsq8ESGR\Q$C>;Bco_9(Z/hHQ0Yj1%C-mEQUBS]KG+0i/)_IKq*J(Ekj>HJhBeI"qDSmNl'_eiN`qRFK9Y)i/VccfQeS$a@V&3X]m'cT\ig.M/coC)(CU+Jip.0bfPA1Kb;d'/DUi_PI`*_(HuA+quMG%;mQ~>endstream +endobj +30 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1320 +>> +stream +Gau0CD0+]+%0"PUd.$.e]@a=lN8^4#kH6JiBdQOnVI)q[/@GOcCp.cj%V.?mpM(MgMZ\t-84/.)b=[(+Xn2l'D>GsX*@(V,7aV""!HV[+"kIh-0Y3"27ZB&1[b9oUN]1n^%/RZO_ZntJjQO#3H>^LP"/k1g3s-:h5tCs+S2EhbgG:WXaFjr(0ql@c_>4`uL<^>oMBiiaV@$6MZB3O]!MdspHE0@06(_XP=D&]]CXW`Fl<4A.#&>LF/2PTF%$I(]4sQ/#%Rm2OY/pBkk6Uf+;4!@MeBgO8?k4^LkmTA>.p*&p@d:aI,)["DM;8eYLb`&=d>3P@]^1qCnB@/@lhH4kDF8ieh(f#T1N**0m5r9CTJ478Fo/t\XHi;n3\"LRfXaHGJ9%!HnO3oZ*OZ4@!'s[2=GM5>rmUB$FAO0U*7=nMU2=]c#E5=po>`]?4=mmC:rNiOZq7=>k+'I!&ld2"/U4(k\qVkbg[Y$OFo6eC!iW8OrmGb3Y\5uuYMJrb:gGMM.G2B6n+o2f-S]Re5"L>;DiN_n9Qo!LAEud0^jdep&HtNOG_-g5>7nn9XC*MH!mMfjEAVCopb^kIGKmDKm(\S/qouhhQ\r5KpIDZ+NP?r:[OSaDP./SXHX]GYQUWgM.5*(s-C6D<7OLCO?.Y4+lk3PtN5t._X($Es3nE>6hQH`T3_s6rhTLn-#q`$[Z'q0dM-\05Pi(=%f#!8jI!8c*bLap%0?62J;p,'W&V5OHHM5Ps#_!#g&lOa8/b'@5L?8PPd[KCX13qA5:'Jiq08($7&NADLRMP8ffKH:>u=lYMFrDTMcgH\qe_]6fTdm@M.Cf'AULe*Sa%b/CkCp,;LhkRe$Vd3FI-P"100;mcun2A.$=JGXaQ"ko6Y-X,Zu0p$gLlD4AI,5$f*8:)'j)or`47d!@Pk`u!coeN(aI1jg%*qJuIMa]d5&QnE`&Z?Oo2J##_QhQHs:IEVQ94Z[b7N$D'F>:bN"1R%oG'*5n%bIe#ip\i@Ulgo)I43>CC9Bn>I[nA(9lNdI7`GZ*P.Ke`)RmVf(3nA_2h_RWc4"=6Te0#ll7f8!!5%V\+"-7Id''DPI5Y$hha=#*B\28t7=2'6_+rqi=OEiWjGF_^-mNZ2P;`n$>f8?a!R#4(A"6j^im^P"p:MO3Y0r_-+*!FQiCjdRHP_S&ugdM~>endstream +endobj +31 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 729 +>> +stream +Gb!#[?&tF>'Rf.GgmBusP5tkn"C&=V-JcCPRVY@fjF\ul8f&=*%(_2_hg=mg&XtWF/q*r`;/t`gq_?,A"W]8-IeFG*3u>8N$If&7"?KM;i'+qX=8AuFm5Ia,2/U7.0HAaq[7qUliGI,N/U_$E+o_86W&Lq[<85nmedr(@]AH2cVmR+'/5cY$cWN6^SZm2B2'oR@endstream +endobj +32 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1335 +>> +stream +Gau0CgN):C&:N^l3dt5:#suMa*E">ij4%+Hm5eB"0:![aWPN)?OYS_u9i:h8H?A+Hgq4s;`>j0fANN4*LUmAC$:%G*+T&2L+kJg6]S.7sb9@hbW@i>SWlBaTnU63]+gS2n'MM:#TKL7VCq?Nmmr-'j)d`l&!kN!YA(+TY(MVo%%*9>8t%No5aY!>f^-(fFfjW;K/BcR=6'Ss;do=l6o1YNoHe@!hkLc\gP1Q\sU74.U7&[TD0g!O](U2nLZR997Ih%j]$ba"6^L^74LjQ>ecYpa($CjnhEF&H$@r5@gPuX,K9@9op:U5Sd<_)IWYV)+SWhOnnk5C2(C;/T\Z`rOj9C;*A66DGMV+/,de3uKCQjsuL<'pM@]\Q/UFs8Bi=m)>i?I*J;e3+cm'M]s97AL,Tj6fALjrCm8uF[$#$*mejJ`[p%Wnd?kq?h3Lf(bAVN=^r3soVtt_r0YD,R2:Ju39"GOT1:5[&<.'<^sl'QeFP]6cTrX@)N@Q+X[2s5Du9?I/h;+:V.Ft/cn8EMomKf^@!Z6bGB6>j%?PQZ!XqWE4JV80-p!C[s+W.)glJE<\=V5SmqV\4qSP3o4i9aT6R;,RG@:-E-^S]gPK(Y*J/e)BpO63n3mP9YIP[=u/4o+5SYPk)*^%pc2F/SZO!A[,XWKgXbRp+L4'2C3GEc(WX+(hOett5-:eWT30_doR1#;YN.BiTg0m7jI@^gK/II0)?IJUP@]MVugQ0nd_;)Hn2cbX>T\4jliG/p5`SXNa#qUQ(>AJd^-Qm!o4ngBP:Xl;EI$@F'$(Yd\C!BZi$:(CQ8Hlq;4]1OrHDLC)](oD'T.1CX0$,fb($1\poO~>endstream +endobj +33 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 630 +>> +stream +GatU0d;GF-'Rf-p:A[F/@5%fkh6KY@QDmb>XXi*=02?IQdP"&ZR#fO9>n%Io:k*X$7%paZ4'ei>?N[UieF:-_LIAF@IP$3n^IE@Fq&g`"$""tcRM%X*lVb*Ds?BF(VIseXpLkS9\aLdE.35fE@#ro28qf,/9>m?bmBK1__et)61-./he)T>5t:hk*_UB%P28uSYQco9V$JFin$H:WO&@3S.@:s6/_!,TL6$8\j!lgaK/-I;%I2(cQT.^S;`f#r@]en(`+O_4261Af9PbBeEiAfbLMK90gn4/&WsRi`oXrl3Z2eMTZL^Q7mE#4hS.9M3mEhcRi&k6kqVkrkutdTJP0Nb[g_:.`r1FM94!oY1#haU5W&V>*qKrj'IA$K[4XWThF#na+0"3Q$E=(V4.s&$9"6Z%!48(tb&CG3@UE>WUendstream +endobj +34 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1425 +>> +stream +Gau0CD,91]&H88.0pb\p"A@%TX,'"dQU7R3n782>41P/j5m,NdNS^IE>Vk[(G=%:$orA+0Ba6k@LT]EYKL#;]Wo2#k,cEfC)DZpI#b/tP06us-\F]O4Du"F%\/>s3f8W[^e\ol5pE!3A$oHNTIoAlW\/09FoUP*4'm-=!++!-$9#)S5;u[LDB#\?[_&+5Ro_(9"FHa9GJ"FWBGkd$$OYk^I@b\S8oEfsnq'LR]U,SPgT,Y(XQ`G!X.oF]1Zg&K\i`1GO5\^,g$-m+co_TOp_K$/6jl7LWF8#OMFGeOWkis0U5srM1W:/j\O$em?/`Z&a_3W9qiKdib&b,C/iS(>L@2B]kH!T#J=a;Gi#C&lg*cm35a_gp5c99B"_#P6uNhDcHN25&^_(7HYUU'&RbV&-X1(TM]8d8UK=EA*SOQGN=`eo^hQX5c%"bp[BD`>BcqZ;PUoh1dB/$)6OSrBYs^(Mc1%(9T,MNO\l[a$/)Nqil$@W_uT$Lb"*lOFI)M1N5hdD$1j;OP)Ugl-`;T,)pGmV9T>.k@1995[NKnMH;(ntq)=C=rQ%Gel9&fstc![C).s7]8!:`)lpPKE!6IHalWpFS'(HT?ob2h?:#Nij"5L(X!27N'Cm^3r,SfYMR;r@UMQ>%/$.?^KsXS6F^7AeP;>G,i:TR#SAoi[+HK2Ln_<3,$m:!X53`JGl-jE923A4hP5!n8E_4BT9P6_D`ZlW>aEOM3N^*QO(k->?bP11t>kU\7Cteml-[)42,-H2YR7_1R,ISto,-pG0:Nb<"0,QAYr(rWA1fG7X~>endstream +endobj +35 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1337 +>> +stream +Gau0C=``:f&:WeDFS([Q'F9@H"qn@fPLuj6AaS.>AmD;9[:$YY1,Au0'`WSc#=fqB-Q=qVdAjS_\8:D!X3nR&3dRkd>KuGY$T?W6Y6Fk+V?OZ`W*VCJYdoWg_1cncr7%2PHBZG.dV+.rq"2:>8"<-s,LqrF'>'h<7Q&\sEd\Fai=-0:UIseg>H8T,nQd7LL,MP']fUl''0u#7oIY+(pOn,Yd#"o7$=Nt$n/TdXZD#]1UrV)IhKsf?Oe-]8VCF3fn.AUSF.]:lJ^4NoXp"Z.Z2(&u]UbaQk\qc^4PTkO7bBBfcX=JM#17V)$p@ZAGk`#H$u&?W;;EN7O3gI8mM@a_J+hh$)p]&ZAAmRfhQJ_\d'OF!g>cUB[`;)0NZqN]j8D"hMqJ^Ro0FN^&TP>gG=UC5@Q/I2/e%j;nAKC$bVWf8Lc4fi-[tpD/pZd+$:.!QfC]j3%J4hd%t'RWbc$V!/pC$[[D<98`Ad?rmW<>4h(Pdrh]-H]'*F50^7ZrKD.j&srAUR_/5=BgQhMjA;8Y603`G87C/91_Dd.&Gci7B)7!@r%"P6;cZ7X(ZJn1fCAJJp6WVukqL?unMhojt+Y`U,W&MEabLZY"3%e*0,.FoO"bL_iBAB!,iX2]+MZ&`cXbG)*UQT#1@g/EHZ8-_.4D21Rn.kG\7Z6_7ZE.c&uplB\3._UgFj&aZQ@>D,kUgT5;u#qHbSUbCrfI_!^.kEAUk]#1f2:DSmLeP-^N]Bj:iDRrpD4g;U3+0mhNq;msDQk)Y*^:=lU/c*us-jW*H2c9as#&#))1qa9:Pg+b/.VhPE9+WtHp``%oRW`S-p15,G.AJ:t2LR`Y"NZe3'm]Cb^PE>M3faj<([&#m"(:X[OsJa@N06Gm'\G;bOUoKe6EL]ha#Nu-4=fGEZFrpMQjDjZ.tD-FBbU>W7YH!5AbE+WYs-kH5r6]27k%dqXM)]Mp_@3ChN1E.mkendstream +endobj +36 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1211 +>> +stream +Gb"/(;01/N&:WeDXGl192'GO?Kdo,^C\>LUWu8q_*k[R\>e/Xdab@/Yrc^"%["Kp;@h*_p(qq8^h!OOsI_3.s_,=W%ruZsqa"B2ED6P%"7prgM(:YIEX]rE9g%.;[NnNh]?$/rDCYITf=$8b2-r.\BhB=]C#Fqh3MKj8&UX1(;T7KQ_DH=VdkiqC*TKR\A+ZHtft8s^o/Af)TDnfk/+i"hl)u1#MFlLD3r*\@U+@W)1F1A@0k_9!c[i*h\6@l!(c:lRj`]otK%I\=/q@PCp`iLl(g]Kn17G^/72qD%4,IYlb`c)d+WHu?1@.=g>6em''8c[daE>$85Mm4/b-9her.*]]lmtZqMUaUg.]e9a11Kr?cP>rZTZUp@cgP).57L'$rS])[1$!m*_^f]a8^[/uHfA36Et\4F.0Z?MG]C#X-RsaX;auP+N(eGW937Z8'[nhDDO%TOYXDoF+fV3P33]Ji&[)UQA]XZ17a=BEK'6r2Q-MGc[e89i(@es>]4cEU2/dK"WLuP6Xc_lX<>+3XDk[SFAJ@p?Bh!HUPYr&]1WPj05E*h2_*O?$IKo1YsISdADlhdNd;oe`GP(V#&$04[/8rY`NdB/o6YN("niaN\^3B>Q)XgNiJ.09]TCfs0@\V_3@`=ukJP`a3S#Y,=$!\)LkO]`\Te95,endstream +endobj +37 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1621 +>> +stream +GauHKgMYb8&:O:S9`3tI0[9L6"e%?0S>9/+YuIA]*!@d3/5[V%oPuD$q>/Ah.>`(6;b[:^Ybh$Zbac"\fqAQr4dQ(/blD(0iU"tobD,Nr=K^W"GR/rldI!(Hm.>/4O6jr>^^CQ$nfuR-Z!mNQi-JcjU$4s'(m1SB5g580VU]]CcH(\K[W\AD%cQJ+@LCsPN3SV75DSb1`\3S6:\=#FmC<"^o"B7"+)NqJ36NSC(-MMB#gQb/Z[C"^eDMtA3*iCQ.Jn,\Ie3_[h:aG$];=>0g?k!n!b2(f0NA*F?oJ;fQDRd$*K$\@$*RJA'?jm9YY5oJcOKs=q1`TFIeU[hen2he*gegp/_Fs$[)q3:g4ZT@FWB_eM*JQV_DRdcf1ngpG@9EoVB(/`_E!Oaor?nN5;'creH,c-a+,\8L76-nd$XG/7Dd'sXi(`CV_-_:sd5b$YWpZ=mY@G@hPu:!c:a?9FMAr!Rka?=$F*?C,LAuV$NJS\Q--:A;i>;S0;ja!\;s2@0/#(1B<"h/ftE,$]fYmY%.Y.7kU)'7O-*7Kgul4FHp@CQr:T)WYD2Ku^-Wu6pDXQ`\:P=s[DY6Z[\p7OWHh)_,g_NY+CVfR(>fe:+D*8gOdl$S*FB7GW`^XsHWVRVAZB*[S6JP'&S\@=Z@I/f7t[j^+2Z%B8?r;%_h&cGe-TL!kgL)dRc-Ht[CbfP:#k.?A6dZo6km[qJGYJr-PlrLG+0*>3;.>D]H$s`3ge_"4UA@J`12;WO^)U8P)Vj)**435q#7fL[Hj:Z)T?7*ndhK:3_LHb;1mbeX=n5A3qZODdi2,LEl)W`G\Xn()"-4qh6(CIucMJecfDf5<\/<*k\@h\-BDlT>F%]BT)8FYhilDBM2+k[%c`8upL'MplYp.q/H"rba8;k^#^teapg"P*4dU>\+,qY6_DFE?AHaVXbB]NibBXnnX\Y:BJbDh=tM7/5+)j.Z56S6oa?jB-)"l\fUE7PT%^oScTE,6;*cCa_28,iSS4]Z\~>endstream +endobj +38 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1634 +>> +stream +Gatm;gN)%,&:O:SD#G""j3RpR&Xg,jini*Y'5O*Sc#d>*Q6Ck\9gVmE:B/!8?:7\W9M)3D&d;e>loE!:YU&/d7EY(%$Li3?QWF:qKuk>Hhsk.G&O&l`*`#=D),_i@BhVnSN#`Od1&Bk96UNp.")tooojS8'm+FVEk9l'W3aj?aL7gfT&GGuGN.,)j@`@qo1RK_$Ec*+N_J<@TcS'e$W7qbP]J`#Y`'o[h2Xd-Br.Yt1V=L:TFi3fEMV5[g1JB@l_G8l-GZ^\AR,udKA*g&s,N:s+Z"5c'SE67Y%N1`D`IQd@m")4kWkfYUP6EKs.)??T8B,[PZ(eV2oLJO6n-tR*@7G2iRQkLMkN^:rD!>+`DSNl>S"-`DV"cB+9?u_i9e96_qIT]aKb>hE&nhU@=s=L():C#.KpZPUi(bq"b%2dH*,`^s[$o_]JdI\oboZ1A?&(h[o.+p8B)[/+lbhU,B.t>hBq7Vb\YWQ<8@fq:/X]p5n,LJ>pKV`4\\"],)5.clMW)E7bIK'u@n8*(#Gnct_.Qro$[C\,8h:UH_bq>]?=rPi(&Z$u]Ja+CY28k#Qm*%X$SaWg-T'Nm3)"5\@J-9Y?a":HB//d8Ra_nK(C;n4:H*LMYUR(q@964Gh'M@XN\m.MGkFsXjcg?65\E+TlG[KDnY[YUQndiWQrGgnO@I3A\l[,bMroHJujV!m@&s_SL:9,8Y$tr>7Dg#4=aY/Yp8na$@@5n&N,>-X^4pK2D-quh;<68PreUl=,*jBQ^GBn$!bs`n(d"cSW9R[6Q0^SCYZL6L>uU_E@A20a27",;;Pbe"Ld8)MjH'0DWi`IX1q4lWT!#;Y9DD@^\rfcWEd@c&U#^X2IN!G\9>6I;g&p*m'uPIR[@m/9:t0)4j*r=3%8>dJdOXo9g)%):s"sB9L3G]'[)-1j.&`iBFIj6aJ/#-q^?-KT5AD!BrN7mu+K>sd*.,frj'NtCB845C2m`DDeYRl&4mB0F=e%HKCth_;]RTqF`!KhZDF)a[NNkhWfB&7OST&rUFutF!P8uosL$_X_($=#$*]3J\apUuD"4e'?F8~>endstream +endobj +xref +0 39 +0000000000 65535 f +0000000061 00000 n +0000000143 00000 n +0000000250 00000 n +0000000362 00000 n +0000000567 00000 n +0000000672 00000 n +0000000787 00000 n +0000000992 00000 n +0000001197 00000 n +0000001307 00000 n +0000001513 00000 n +0000001719 00000 n +0000001925 00000 n +0000002131 00000 n +0000002337 00000 n +0000002543 00000 n +0000002749 00000 n +0000002955 00000 n +0000003161 00000 n +0000003245 00000 n +0000003451 00000 n +0000003657 00000 n +0000003727 00000 n +0000004008 00000 n +0000004161 00000 n +0000005204 00000 n +0000006525 00000 n +0000007362 00000 n +0000008854 00000 n +0000009230 00000 n +0000010642 00000 n +0000011462 00000 n +0000012889 00000 n +0000013610 00000 n +0000015127 00000 n +0000016556 00000 n +0000017859 00000 n +0000019572 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 23 0 R +/Root 22 0 R +/Size 39 +>> +startxref +21298 +%%EOF diff --git a/Les08-Van-InMemory-naar-Supabase/Les08-Slide-Overzicht.md b/Les08-Van-InMemory-naar-Supabase/Les08-Slide-Overzicht.md new file mode 100644 index 0000000..2a8157b --- /dev/null +++ b/Les08-Van-InMemory-naar-Supabase/Les08-Slide-Overzicht.md @@ -0,0 +1,145 @@ +# 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 + +--- + +### Slide 2: Terugblik vorige les +**Titel:** Terugblik β€” Waar waren we? + +**Bullets:** +- Stemmen werkt lokaal (in-memory data) +- QuickPoll app: / en /poll/[id] pagina's +- VoteForm component β†’ stemmen onmiddellijk +- Nu: alles naar een echte database! + +--- + +### Slide 3: Planning vandaag +**Titel:** Planning β€” Les 8 (3 uur) + +**Timeline:** +- 09:00-09:15 | Welkom & Intro (15 min) +- 09:15-09:45 | **KLASSIKAAL: Setup** β€” PDF Deel 1 (30 min) +- 09:45-10:00 | **KLASSIKAAL: Queries** β€” PDF Deel 2 (15 min) +- 10:00-10:15 | **KLASSIKAAL: Componenten** β€” PDF Deel 3 (15 min) +- 10:15-10:30 | Pauze (15 min) +- 10:30-10:45 | Uitleg INSERT + start Deel 4 (15 min) +- 10:45-11:30 | **ZELFSTANDIG: /create pagina** β€” PDF Deel 4 (45 min) +- 11:30-12:00 | Vragen + Huiswerk (30 min) + +**Aanpak:** Deel 1-3 klassikaal met de PDF. Deel 4 zelfstandig. + +--- + +### 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 +β”œβ”€ poll_id (1) +β”œβ”€ text ("JS") +└─ votes (10) +``` + +--- + +### Slide 5: Supabase Queries +**Titel:** Supabase Queries +**Ondertitel:** Vier operaties die je nodig hebt + +**Queries:** +- SELECT alles: `.from("polls").select("*, options(*)")` +- SELECT een: `.eq("id", 5).single()` +- INSERT: `.insert({ question }).select().single()` +- RPC: `.rpc("vote_option", { option_id })` + +**Tekst:** Dit zijn de TODO blokken in de PDF! + +--- + +### Slide 6: Server vs Client: Wie doet wat? +**Titel:** Server vs Client +**Ondertitel:** Wie doet wat? + +**Twee kolommen:** + +**SERVER Component:** +- async function +- await getPolls() +- Data fetching +- Direct naar DB + +**CLIENT Component:** +- 'use client' +- useState, onClick +- Interactief: klik, type +- useEffect + +**Zeg:** "Server haalt data, Client maakt het interactief." + +--- + +### Slide 7: Pauze +**Titel:** Pauze + +**Tekst:** Deel 1-3 klaar! Na de pauze: zelfstandig /create pagina bouwen. + +--- + +### Slide 8: Zelf Doen β€” /create pagina +**Titel:** Zelf Doen β€” PDF Deel 4 + +**Ondertitel:** Het formulier staat in de PDF β€” jij schrijft de INSERT! + +**Stappen:** +1. RLS INSERT policy toevoegen (Stap 4.1) +2. Copy-paste app/create/page.tsx (Stap 4.3) +3. handleSubmit TODO invullen (de INSERT logica) +4. Testen: poll aanmaken β†’ verschijnt op homepage + +--- + +### Slide 9: Huiswerk +**Titel:** Huiswerk + +**Verplicht:** +- /create pagina afmaken +- Validatie toevoegen (vraag niet leeg, min 2 opties) + +**Extra:** +- Delete functionaliteit +- SQL queries in Supabase testen +- Styling verbeteren + +--- + +### Slide 10: Afsluiting +**Titel:** Tot volgende week! + +**Tekst:** +- Volgende les: Supabase Auth +- Inloggen, registreren & bepalen wie wat mag diff --git a/Les08-Slides.pptx b/Les08-Van-InMemory-naar-Supabase/Les08-Slides.pptx similarity index 89% rename from Les08-Slides.pptx rename to Les08-Van-InMemory-naar-Supabase/Les08-Slides.pptx index 057d5a7df4cc92d5d6ddaa33744eaa6eda64a669..e0bb41c2902ec3f7eaca38c15c0f01a029ae4410 100644 GIT binary patch delta 2681 zcmZuz3vg8B72f|W31qXoCImMT)_4<>>}DUyzVcd85_!zfgeE}cF(oyjU75wW2_eu> z)Ckt8M%0FDIlN=k0R|xj7Uwd;B=I%!5F4jh%9ui+*lM&>M8s(k(*OMbjU-MpbNA#w z=R4;+=iGD8y&JAic>nte=9)_5IFo_?hSqkNvw{Ema_M)W!hh8m*g9THp>vmm&eQmL zFrCi-aqD(h4tU03Fz+v-fju~ev^a_ZkEvf zovC1|O)RjNp7%26{YmRvs_FI@0eM#&a^}aWzB+QPrG&idL z0T1bCs^w>**#Ap5NiA=;#AC>RgZ%!Qdf>>-+hxxFSa1+{OVl9O3xUVIL_ zYMCd#eC9{fzx8x(q8CSfB77K4G0DIIy}1Ot?1(UWljk zR+8vELg zzX5DGtpExM-BbVzNFFbM3X;i%P$i(}`ZSQxRfSL}RITZsXn3WVd%sf%Ve6RPsi4Ux zZ%u^{?6Bq?wZ1nS-zoqzMSY2M&%B2neax;NuqEI}K4};r!@N`I3qUs7{a{ll>b3PX zPkEdM16_BDcjd@WZtQTuLb}4}jfxz`*c1o(d`Ua|I0KA`3wV@(138{|d;YZ;<742Yy|` zC!i>(N4+)I5E0!mzFEM~AAl_Eb&v9TCCCYm`xqYdGTWq@N(Pe*BOd~7OT@Aeg$yku z$^uHkAL2yU7fYf?L*j*vPjaR4-8!A3Cj_=E>KW}3hs9u^Yi!T{vXo)(!_0=sMS9o7 z)(~XACRrcf9G0Mi%5<~d$?+!g}<=r z8e8hpF03nod&ONA`$-h}>f4fw?bXPqblosjTrB*Fy1m8T)Ff_-exBGOnC4DC?M62K3Q|i@DO6_D?C+zkdkQ02OjQ!D_5pZ delta 2717 zcmZ8j3s96*6yAH*U2(znfh_2*x@;mW`(PjJ!v$0?LK~p%oQH9XHRa!$Z>6gTm(WWjmz3=~JUm-RaqgbY>Lx zLAlA^jX8!lKHf)-a_fT0gh}Z;|M$3Ud1{^F=MDN> z8Zm^mD?0emos}(0gq&G3fSX)?`L9F{ebr&970_E-1+;yefWB8X0Mr=v?v?9q%pWR~ z%^ynJZA269s4@1Y-MvX_`24{{LPLkiEh%T8Hfd&{Y;nmmdcQvy@-Nb7*j@T1fgshd zC(CG~iae{ddz>DpTe5G^-D9|@R-F^2{BRIZ;pe)5FrGx8sMXyU^@{q&Nu z?(Mt!LN7gbf{cvlGe{NoO{B?6IB}%SM)c_%_0@jUDIQZ~*lf~4xGu3{)jnZqog1}0 zw}4slKB|rTTV?1glNtK*KB8m1posJ-CS&^~^;}p_Wfv{>`iOy^=`QeoESV zH5rZ+8LP=`Er+|0Yxjs6H!`@WG41aI1ufl1@HLOTzK!rIN1mvPRwGzLM&p|rscmAB z;6uX+hT(knUrv0&-A6t=O?aszJI;`Iai~PbeM=HJd}=#sx^JSInN9_+Gd0yxal;42c_S{l;blY6*tdj zLe5=eQ>H+2#LXz88sT0O;7~=|Jd{lNAWFumqr?o_=%J4K%z~J?cHdPVh$=Z(r&(|f zwm>c-3N0`W^5EeQ$`D1Oc(fq?sXo}$q&yE^lYlAXpu*lFT|7Mvjp)w9vHxV=9O>x zQdI1Xd4jo%1)#^Q{vS5?XSMrDYVpDYoZ!x9>Ipn4=vqueUa(>m)n4XHUMhTD^orX3 z%9L<^x$wf~iSY7#;I)QytxSe1wsu!vpv9x2Uif6;rOT5kY{s_YFy-=Nf^|Yp zl$B2x9NV)MMJH$MYCgl5ahYOFE?yOZeZkl<=bv*y*X#H1iA<{Q zr<2wg71W$3^7iGi6q|biJ!*m!>WqUV+Ter)-T?08D<98b|Fz(XjlNY$*+(Q%x_zfV dXX5a|&(+3fvQTiDW5QW50^y^TZE3*t{{dhVknsQj