diff --git a/Les01-Introductie-AI/Les01-Bijlage-B-Huiswerkopdracht.md b/Les01-Introductie-AI/Les01-Huiswerk.md similarity index 100% rename from Les01-Introductie-AI/Les01-Bijlage-B-Huiswerkopdracht.md rename to Les01-Introductie-AI/Les01-Huiswerk.md diff --git a/Les01-Introductie-AI/Les01-Huiswerk.pdf b/Les01-Introductie-AI/Les01-Huiswerk.pdf new file mode 100644 index 0000000..7ea2197 --- /dev/null +++ b/Les01-Introductie-AI/Les01-Huiswerk.pdf @@ -0,0 +1,99 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 5 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 10 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +6 0 obj +<< +/Contents 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 9 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/PageMode /UseNone /Pages 9 0 R /Type /Catalog +>> +endobj +8 0 obj +<< +/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260603151533+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260603151533+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Huiswerk) /Trapped /False +>> +endobj +9 0 obj +<< +/Count 2 /Kids [ 4 0 R 6 0 R ] /Type /Pages +>> +endobj +10 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1787 +>> +stream +Gb!Sl968iG&AIa;CbHeVP99l#!16EVD49*5:=CJ'HLV.S+c-h#-mWBKpt'-Z4saFAXJD7FP"$'M@%Z>*=HER&2fmm:-,AsoGk$Lhj"Qo%$V18mF80h;IIRT(\-PDD^8-5%J],9N5r&%Q#U9u$4D#_8+Ar=R84KD6-#d*:";=UFQot$Y&.qb_"!Eb\Oc41ZpIW:`WX_Q:7IfgB`]e+]Mb2E#LM#*Z'S\DGj&@DV%gZa?#[t)nPJ>t(5(ccie`D$f9EtMrKc\Pi\DUW-DeJQC?g5e9FR]K'Eu7'J$3ihLLlk5)Yq>TiSX(4#/8?V`X(e"Q?ip]W0SHQaLc9KG4sM6]I16(Fnb")2\+B_6ej6-oiC-F"NU33%YTrE(r]5Y9f3L)m',lVLY3j7RiX6Mh-]PC@T%,:U7`OK>+ul7D!:A$tVOpp+4X`n-i"Sj3umPATq1+cc7kr`Buu\pV&"pX$,]EF[ff<'8nNO'M9gSL80Oo1ETU?8VuiHBeAqf_^YHA`&_R9Bj\BBSVcU0P>Z2Qt/@_;Jiu9=#`EoGGcfY3HCEu8BQY3%rg^1<.%FUN3lcScOdBLgh-D[2]m3BG5r+?goVjQnd^3I$%^u.\@E9%I#EUr=7m2l\C&6;_'@pSV`b)!lNHXOONCo@q/&,3ik#`C$1%X*RL-*T*`p6J%%c@.#$">SIco1f_E[K_U2Q1'PMW"_S]%nG35O,"RA1^`$=ZR0m^7C_'B$WK_O]N.FK-R'Gfh2FJiO^bW>5/63!gpNM+,#L601%"57#G#]YIo)&)h.ZTS2>CBYUdfh#n)U[S%Jq\EG;bWNMp6//,84IWms54ngK)[N,h+iT=IqB6@FYi;)7Blfl$L9-:/2Rn]Kdo_H/Pmo%gC@VaC4Nt.QONafq>@a!opadH`9Mp\Yr>k=*S!m7Tc5N:%9JC_"0&Fq82,-&[2^i-V@ji^NNN/KTc1H9"$Jcp6,Z-j1[CH33?00hQFY:Q==hd+Yu+Y-l*LLJJYb]rER4!endq%i,>n";$>O=4F_Z$h$AS5=WNo-o%u:f6dEc#g3a3`2.^`G'aeTDY)cJ:7RG#(Mib3&u8nX3tSl>_nW`Kr9Pt%s]UgCm:d4j%9G0nZn=\X$Nb$1fS5;pA'Z11&^*ugV`lrW>POdqp6`c6pkYESY\NTF>t*FSC[b2Jli]]Fep]%!!7'!&%q@LYq9Z?*QUf+Wq\Me6T_r*u2oIR4endstream +endobj +11 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1522 +>> +stream +Gb!;d6$(aP%)2<`e/Rf9\_d"*GF_sm1hf!bPGTV9jPh!W#%LTd^jGnM:GrDn^]Z'LB`j"h[/0Dd>"Q=79R.X7<6!E64BL_6C:\3?/IJb5NOO2F=XE+W]u)d&&S*&g0"e[!"9F>,N'5&0l;R2&I0&RS!rA1pa$EC[3Q>E_auuYbcGBf,+/FbWNq-Klpqglq,>jJeb's=tO0MbZ6X/L=#jII=DQ1U+Rd!08]h@94RU?.6MXRi56k2i<4dHK^i8[oqD%2""5T5Or7/cWiG^7fMA@]%q]!_go_a1^e5SpO:15$,VSL^>T2@YW-pAnA28r&/;&$d7qY9-4im*IeIL9CuM/_gnDL#B8O#4T2/=O\m>`7-m"BN^J_aGl.GS"kUa"4$VqJ5(rHn`#lJX<^[j9aqXrJlZ1_YU6?cP7H^fl]=0)E"YQF=ih_]L"`?b,3;TG'^lTK%NfDmH8oG<*M7@9lW3a2eQf56hJ#(s:u3oeR$61rH\>JDo3u6B8#R0+a+NjGpZM=`G.YHj?h60WoVp+EH4PA4+t.ZRP6>cV6'1>&Y-FI:Bm,UE>2h[0'?V(t/j$0ubD@AZh9i'@UXT">(*t%ii+ZK!F:47nFXH]dV.O5lG7/np6[@D7tGu.'Z+!#\Bo6R`NN8lFL3Cc(@1$I%B`'G-R7hme1CVh*eT'-)@)?D\V-`>R:8q[D?Fao2]?nmKOWY3_(gkJGp^nq=RPW-q"d,,[rad[1H>tQ1naMcnF\G@]N%+)Ye&].(Ms%2Y0S4LIrJN,e/qj,D7ih4JtPJ7$`mgRLX$QBOPRuWZD=5LaX&lXq^0QDnLHZRPm"FXqb"%UFV([c7CgC&(05lsW%$KZp7c!J0V)k&E#EURuSmjTb#jgAI+/inFXu4`l(i/kRBBZeg8;lg)pJDW;STkbNjQY>Orpl2%=e]I%99mY'.DfT@_rT&L$8h_e=D%*L~>endstream +endobj +xref +0 12 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000535 00000 n +0000000650 00000 n +0000000854 00000 n +0000000922 00000 n +0000001207 00000 n +0000001272 00000 n +0000003151 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 8 0 R +/Root 7 0 R +/Size 12 +>> +startxref +4765 +%%EOF diff --git a/Les01-Introductie-AI/Les01-Bijlage-A-Lesopdracht.md b/Les01-Introductie-AI/Les01-Lesopdracht.md similarity index 100% rename from Les01-Introductie-AI/Les01-Bijlage-A-Lesopdracht.md rename to Les01-Introductie-AI/Les01-Lesopdracht.md diff --git a/Les01-Introductie-AI/Les01-Lesopdracht.pdf b/Les01-Introductie-AI/Les01-Lesopdracht.pdf new file mode 100644 index 0000000..0cff1f2 --- /dev/null +++ b/Les01-Introductie-AI/Les01-Lesopdracht.pdf @@ -0,0 +1,193 @@ +%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 10 0 R /F6 12 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +6 0 obj +<< +/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font +>> +endobj +7 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/BaseFont /ZapfDingbats /Name /F5 /Subtype /Type1 /Type /Font +>> +endobj +11 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font +>> +endobj +13 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +14 0 obj +<< +/PageMode /UseNone /Pages 16 0 R /Type /Catalog +>> +endobj +15 0 obj +<< +/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260603151533+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260603151533+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Lesopdracht) /Trapped /False +>> +endobj +16 0 obj +<< +/Count 6 /Kids [ 4 0 R 7 0 R 8 0 R 9 0 R 11 0 R 13 0 R ] /Type /Pages +>> +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1890 +>> +stream +Gb!#\968QQ&AII3oQ&RkPSl<_uBAsO_%+5`3E$=Lf:`>Rd'"erO'kcU_g5kK.4U.I<]eXJICZPJ_SD'r[#5D)da"#>iF?G0M@k#07`kare##t,iH2VonC^XRN@Za%CY=k8md1CF.UE8a:O)e$kCL&9i'KY,^3MXDOn8(.g2i[RH(mXk5!^pWQk%&>?aU00)nK[mGPBNNE")5-?hQWqGTA#pXeR9Gb'jK%rTfPL/16m_+as^6?r"d!aae)j)g%/6g0T)s%"[(#dk\:3L9'7$baJ@SV93Y'bSgGP=#*qu@j`jrk>eL536!i'fu2JQgJMV1TX>@i@oOr6!ffDFkE]h54ZD@B,gj&-5BHkunqX+H2(B\]6K_BP,.-bI)#O."FVs/R.qUs*8%o.gX&'U(l&q5nC*YiJCL;f_=bIO0>FhGQs]G"4WaebH[GMC]4[TSQEge)0-To'JV:KR&_.0?Q&^gQb-[Drfkaa+7rhPQ`E2/3h8G/'K@31n,Oor(4>(,[iNj_@eT[ddU[4;c@?tBM:s35oYO/DP/>`g9Ai\J%q_s7FeIb9YkoNAY5\\#p4@![!ete\,eH>]3#`YYh#qo2?INq;*[rr=NP7Hh`\p:nIF'KCkVGPN(QNGhPMIaOOp52.rY`@3bR"EconikFkZq]-M(nk\2@hUNDQYbHcNV'i4BG3Wu4R3q?CP?MXd9aB_;_;VY;M2^/GU&2hYe1RZn,;nb-F+B07>!C(I0C#qP^/1d\gtqO2cfX[Wp<0,_FQF];=H@KKD_#kKGtN(c@co8n2e>-s9@7V?C>NNqPVH^N6W,6`F._;ho.PJ2K`(,]L[_Urb$WS13cO>-A-"0/PSPugX@aFDfFh.f`4TJ;*E03"]C3_=[DZ`1Gt(EqsApl=QWe%Ne5_!Cm?X$/1c+c&IMRp<4cDcdL/^mmXdT]8m=`?\KjJ^Qb1eNhh1O^FYjW@\UYYJ?Yh\t5*$6m(I#<>N.r/him4K=^qEd.aL;@koLA)tpD0bD"!:T;HNO?(^H[m_(`6]g0)"ZcOmr(E4-AnK5i`Hdi~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1729 +>> +stream +Gb!Sl;01G^%"@rS;"I*0\W(gH[b#e?)q+@2D>X3K=>VZWA.qj./D\a"Xn>-N:;77,D@"DfoOH6IW>`X/]#WO6eB?HP`'k^,B$3WNTqqQrH@JJ")^jOK%Pm!tolRD;#k8i[k6)l5P4YPH:IYgiC^?Z%PK,-+l,/n_WYnk)lfk)fU+#?J^Plde!P#auJ@[Jl=kSG>cor+rQjfkXlUAZroqoVKC8I"#qWqVT@4>?*5BGJ3+9(CgRJbh>`BULI`^!Yu%o4Gmpn!oK3lSoFtjp)iZt0L:L"LnHQF_CXQQo8O=L1Y]Bk8dNqMlAt"s:a%$A^-9>^*A[&Z,bMM,R+.FEA@55lPp/,p*@?`J@G]Y*&-+pmNKi*'bEs3P;tOds^-<_JAA)1s=q$d(oX@Bn>MWcsFM`<(]7_B\0GnNE=OSK6Q@[1dG^7jg&pu#R=IdEKVI,",+slf,m*1_iU_<4NCNaK@k12mg)c`G8YK8MMFKVOQCeSPn9j4A$be#blOBlDOiBS$i<@tiet_J$"="S_N/3bl=u!!%*mUuX6[Od0Y5(__W4H!<=U7FP_MMKh]Vm[L4b1r"!'+Y6$.E=[A^)`U+1l!04gM^QtApWhSQ[)0b6/Wo30Q8F^)+e'[+XMW_j3&%([eY.8G4Am_UMMHS=j-+bNuYaQDco7kq9K?'kR`EJNP5/-bt#ltHC!#frf`6p[2JM*MOg&0homC?n&a*(HJtJPd:p!BM6grCa#",C1pKT9u!!!U%FQ0N-u5V_`ItlR5]h,7YlmlM02&Acad4ot8'CVKu8aYg$$@BA6VAJ$U_c,+T6RS_dqJYb04N[B?9`UHC,%'cY(g=N+sW[H\T(0&(KT7@sa'k>^j=3XN<*fO1jKN:=YkdU/he7i?Y1`Z3$'18Qgns;1LiX@G]WG0)6<]uq^Dp'IikM\!s1G]O'1tY\_:WY0m`n'nj1G%k\#nF@bE]p*3*.A!a@Oqn3%<%R<,7B]j`sd#ohMW8]g?GU&XIe1TOe#a>h`(XAfPH'GZYiMSg*mU^9bAbM/C*PpSk2X'$uW!tmPm"*RF(5;RTO=[:G6MDK_t7NjurG0H#-5G8r@AP9>_CORiAU2"ZWtUrV~>endstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1609 +>> +stream +Gb"/%9lo&I&A@[:lo6el`"PN9!(kPHfWin!m)6$UoFU`sOXh:b7Bdm[-iWdEI`cQ`0QAb:N[9YjK#HE!h8SgR^nn6dr24=\UDZa#4n*id/\pD*_s@e,B$R"Yo-6K^+g=5A6/hu>K),R]knF[u/5#iY"6%6n`#Q;VQk5)ST@Uud(P>"UloQ8m3-&NFJI*ug7.f%+J471JVE#$Z./Ec:Nh<97,qS?6&^C#>@.]3*D/)o!W,^(5OEPqFj;#G;I8r4)3R69LP"5hKP@MH3k%)f@JK[M]D::o0\sB@.!GCDV[CujUN)E]l5nf`#rMAGN6:kU44P5L:P*X=WI3&"#\:K/E&l#4a>n3m7RP6@>:Fu2n;9X+mqI9G@9jaeB[@h<]lqF6,6h<.EYa:JM!_R[eMgkd'2Cd)`H[q%IcO7Uo$27h&:+V6QMqH:bC+.KYT.>u[:NP*:bXI+Mrb:&)3Rmg6fs3ouq0B2jb'bNWau!aZkO0G1tP^`/f5NEX0%-rULhZnS9)\PYd"a4'8Y<5b8He3TWu\H9oAWju,$qc-jRbNEY(=#h(Pql(KGu["$@B3'lI6j,1g[Ul\Ram@AX5VKUEKq5;rjXhl&Deja@UmF>(j43g]$8LWLtXtWr/X"iLOl3BU;;R1qJDH=L8>^&X?F@lI`'8i*Q];B.1UGiqM5W.e,=c-G.UOGOLe0^r$)(X=g(i$>ID)2'tk4/]a1t3Kdl4tbdP2&9DND(KlFiY8R*=FB#^Y]_6>"O[fmO&OG'fR&\8hC@CrSpI*"3l'2XO(Ka\>AqRLU#->h@/6\b:6X"$SbE"0e-.`SMKOb,-2qCl!4ir`)aprCi!hiM`K`~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1786 +>> +stream +Gb!Sm?!#cM&:N_C9R&V@U7KZ&fC,\VFEu(+2nOU^8VlM&#lqLY4UXG3YkeIXR$q#1"iHo%rH,.TC'odJBS?#$qNeF#D)s,UsHjf6;9gU"=Wu0orhV,#.TuLdP4^B>I(H&Dr%phfBVbr&>=o[6?q2p?!Q(N2ho78l$2V*!T=j%4^[CX3AG!@G.-[5SP=d%.UoQ1?N9Oa"Xs(Ll^MTkD??*l175_(eq3%gg"Y.5,J-.\_ieXGRS4bip2a@HVMb_%H:<5/gS(*\.2oL_ITYMPP[]Qh$HfAWl#G`o;lqtB?dcP/t8uM'd:m?8B,]FZd-p)^"-$GXQZ1Ee8MG0:'bqpkfiGrWt+*cV#hC%F-AEG?e_nN3,SB46n3Do>tk_ldL9$T]ZHodNDi9nf9*"k187D#)B*R":R*s757\LV'Ze.U.=6gIPB'cSGX7`(5ZhNfHBa5Z"QE'D]]rJ2ifk.>WSCBkmb!6s$^dkDj!h1@.8A3Z2YA*p6[\a/JX]F^!Y\T0dFDncP$JKNRk$C2Df`5S-rHTarcWo(-G+T;s+8)1Q.L:RH*Mu8PgHr.iDbarI/SH?R8h\,#k+lFmuGd:hkE3<``;[_Q=V_;cNnrlE;*0G)LsA)WPimnc06g)pqSn+ahqmpkdVO5o&o[>F9;NeK7&X-JE$VMRs_:un+ko]lqFAqNB.Pu"?+O/QVdttS$U@CqEZX1eRT&gm\"E<4mJN0T"_uddgJ'0&=k/HqL8XI6%$go,7H$4b#LqnZ_c(:bqHb1_G46^:sF\^*glQ(a=LU6PD)$P?rWg+a43>a]g+^3_=*++;e/1pendstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1811 +>> +stream +Gb!Sl=``:f&:XAWk[Io?V+@A.?a^Tf:9sD%'19'*>CIHc#0$oXOTo(\YMUB-M8KTM2WYn_gDi"4?[mKb"XZC6ibpXWGhIl8k8%/.qEqJ,_h%djSFo[lS/BI[^>jfR#GrLs0LB"D5W1Yp_=\3iA>'0Xko53BJ.l$S&;1'gmY/a57SWJ[q;i)6X3li$4A)E7]bj,Soi6o5/(BY3?MpoX.hL?#!6h_(=rgC+E9\M:cjh*j_r!2SmcpT:`G^A7=-YQ0%l^#hLfbQJl"J@',H@o%>/?g6@"Q.EB*'k_JN-/b_c:o<$LB.%A#'7iE%?aT!U&.dUH<]aDW+c=B-#*br@POcN(2qLF6OV:rKu?&HogETonP_!10n:RF(4A-qm5;XhN8jj5+kQ:8,6+;aq3$L/62(?R@MP:4Fn>XKHj14bqPnOEis"qeVN96#b^oiOXb=&aTNcZ@3gROi^LA=i`!c-+&X@5b$L/[*_i6C,f)Kpfi*2MY*1cKjLId,bmtjg$e'oOU#"du;GEW7165-^_8A96Fk0iLV3i"I2;35-;8UdaUn`<G%^NX4.(k%?\\YIP[P,Q#W=7#aq/o<:[E(5.kPV&Ki+L7XU`TF(U'dn4Q5%ZK9^e]Vf7u-Afg5)iGVf'*]Bu>4M#87OXHofB^o'-%drJFfp[ILfj9ATg/M1"WbX??SRDTl\,X.EIIG'PV<@;r3JTFB`/TQ..9%Cig7"Wa>,0L^$fNaB`7O3-r%mF:fBq'+p9D@fuR:IdG_%Xm'6.RO!kO"Lp&?5rF&R$,dI^.:&2FC\0oD+<*bI`uOd3kV7$(h[::-?T4"\-4d9:(8ql1b%mY_%DYPkURf3-K:+(JlE.bL\+r$TE3;T<8fqI[`U+\NmK_C5hVf`WFD:^p&35cN9H`$iY&JIu0C(@nat'%p*U[Ri;UAWnOZG(88GI]V/7=Uk5o.:hDPf-SQsB:k#8iVqD&<%[cFDho1lJg=0b.*+DN2WOU,F?YY\]b(54P8+S:(kKOJ&8dgQbcsj$tr'pi^:e)GB1*U\5dlmO6b?UsJ72Hn,?lL%+BA#4'cqP?Qa@'"tAht]bOG3GPQA)nL7OH#@h@s#HTjhLH%P>TIYKA$s-iX5EY&P]~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1047 +>> +stream +GauHJ;0/3d&:XAWfR(/K3tiJ<+WrG+o\*V,m9JDFP:+*V=>2HPLGN4DS'JFEi5-C+k8%YrKQ)5Imr=&H?D;]Idqb>bFTD]>1@SE(bO@^ZB5I5r1W^ErCJpBk!bq`+NE!PZ;.;mE'T,>1-V=97735:p5e3POO/,^XK]omZI+D:jBQR8j-hkIf")g%o8e(oLrU;jADZHt"JCnc>mJ7+dkV0/[bB8fTjuVO2\TRc]$"gsQPp5s;D-M\^!oBO:#/eq)R:_IGgf],BfWP3d<&$c)PUd#c8lPERL(>*;;G?)1%D1r.Q6J8riIYFmmT[UD(UDi?i,.f[]4MPOP=<5`ueUtcG92jO[ij3[N,g61-9Uq%*6m[IIHDfpr"Vr&t'BpIKC.jHq3Psue);(bS~>endstream +endobj +xref +0 23 +0000000000 65535 f +0000000061 00000 n +0000000144 00000 n +0000000251 00000 n +0000000363 00000 n +0000000568 00000 n +0000000673 00000 n +0000000750 00000 n +0000000955 00000 n +0000001160 00000 n +0000001365 00000 n +0000001449 00000 n +0000001655 00000 n +0000001771 00000 n +0000001977 00000 n +0000002047 00000 n +0000002336 00000 n +0000002428 00000 n +0000004410 00000 n +0000006231 00000 n +0000007932 00000 n +0000009810 00000 n +0000011713 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 15 0 R +/Root 14 0 R +/Size 23 +>> +startxref +12852 +%%EOF diff --git a/Les01-Introductie-AI/Les01-Lesstof.md b/Les01-Introductie-AI/Les01-Lesstof.md new file mode 100644 index 0000000..f07ca9a --- /dev/null +++ b/Les01-Introductie-AI/Les01-Lesstof.md @@ -0,0 +1,376 @@ +# Les 1 β€” Lesstof +## Introductie tot AI en Large Language Models + +**Vak:** AI Fundamentals +**Opleiding:** NOVI Hogeschool Utrecht +**Cursus:** AI Developer +**Volgende les:** Les 2 β€” OpenCode + slim werken + +--- + +## Inhoud + +1. [Wat is AI](#1-wat-is-ai) +2. [Large Language Models (LLMs)](#2-large-language-models-llms) +3. [Hoe LLMs tekst genereren](#3-hoe-llms-tekst-genereren) +4. [Hallucinaties β€” waarom AI fouten maakt](#4-hallucinaties--waarom-ai-fouten-maakt) +5. [Het AI-landschap β€” providers en modellen](#5-het-ai-landschap--providers-en-modellen) +6. [ChatGPT effectief gebruiken](#6-chatgpt-effectief-gebruiken) +7. [Prompt engineering basics](#7-prompt-engineering-basics) +8. [v0.dev β€” van prompt naar UI](#8-v0dev--van-prompt-naar-ui) +9. [De slimme workflow β€” schets naar website](#9-de-slimme-workflow--schets-naar-website) +10. [Ethiek en valkuilen](#10-ethiek-en-valkuilen) + +--- + +## 1. Wat is AI + +**Artificial Intelligence** is een verzamelnaam voor systemen die taken uitvoeren die normaal menselijke intelligentie vereisen β€” taal begrijpen, beelden herkennen, beslissingen nemen, leren van data. + +### Drie generaties AI + +- **Symbolische AI** (jaren '60-'80) β€” regels en logica. "Als X en Y, dan Z." +- **Machine Learning** (jaren '90-2010) β€” systemen leren patronen uit data. Statistiek, geen regels. +- **Deep Learning + LLMs** (2012-nu) β€” neurale netwerken met miljarden parameters. Modellen "snappen" taal in context. + +Wat je vandaag als AI ervaart β€” ChatGPT, Claude, Midjourney β€” zit in die derde generatie. + +### Generatieve AI + +Het specifieke type AI waar wij in deze leerlijn op focussen: **generatieve AI**. Modellen die nieuwe output maken (tekst, code, beelden, audio) op basis van een prompt. + +SubcategorieΓ«n: +- **Tekst** β€” ChatGPT, Claude, Gemini +- **Beeld** β€” DALLΒ·E, Midjourney, Stable Diffusion, Flux +- **Code** β€” GitHub Copilot, Cursor, v0.dev +- **Audio/video** β€” ElevenLabs, Sora, Veo + +--- + +## 2. Large Language Models (LLMs) + +Een **Large Language Model** is een neuraal netwerk getraind op enorme hoeveelheden tekst (terabytes van het hele internet) om de volgende waarschijnlijke woorden te voorspellen. + +### Grootte + +- **GPT-3 (2020):** 175 miljard parameters +- **GPT-4 (2023):** geschat 1.7 biljoen parameters +- **Claude 4, Gemini 2.5, GPT-5 (2025):** schattingen lopen uiteen, miljarden tot biljoenen + +Meer parameters = meer "kennis" maar ook duurder om te trainen + draaien. + +### Training in drie stappen + +1. **Pre-training** β€” model leest miljarden teksten en leert patronen voorspellen (volgend woord) +2. **Fine-tuning** β€” extra training op specifieke taken (vraag-antwoord, codering) +3. **RLHF** (Reinforcement Learning from Human Feedback) β€” mensen beoordelen antwoorden, model leert wat "goed" is + +Het resultaat: een model dat menselijke instructies begrijpt en plausibele antwoorden produceert. + +--- + +## 3. Hoe LLMs tekst genereren + +### Tokens + +LLMs werken niet met woorden maar met **tokens** β€” stukjes tekst van 1-4 karakters. "Programmeren" wordt bijvoorbeeld `pro|gramme|ren` (3 tokens). + +Engelse tekst: ~1 token per 4 karakters +Nederlandse tekst: ~1 token per 3 karakters (efficiΓ«nter dan Engels) +Code: ~1 token per 3-5 karakters + +Test zelf via https://platform.openai.com/tokenizer. + +### Next-token prediction + +Bij elke generatie kijkt het model naar alle vorige tokens en berekent een **waarschijnlijkheidsverdeling** over alle mogelijke volgende tokens. Het kiest één β€” meestal de meest waarschijnlijke, met een vleugje randomness (temperature) voor variatie. + +``` +Input: "De kat zit op de" +Mogelijke volgende tokens: +- "mat" (85% waarschijnlijk) +- "bank" (8%) +- "stoel" (4%) +- "tafel" (3%) +``` + +Dit gebeurt token voor token, totdat het model "klaar" denkt te zijn of de maximum lengte bereikt. + +### Context window + +Modellen kunnen alleen X aantal tokens onthouden in één gesprek. Dit heet de **context window**. + +- GPT-4o: 128.000 tokens (~250 pagina's tekst) +- Claude Sonnet 4: 200.000 tokens (~400 pagina's) +- Gemini 2.5 Pro: 2 miljoen tokens (~4.000 pagina's) + +Verder dan dat = "vergeet" het oudste deel. + +--- + +## 4. Hallucinaties β€” waarom AI fouten maakt + +LLMs zijn **niet** databases die feiten ophalen. Ze zijn statistische voorspellers van de meest waarschijnlijke woordvolgorde. Soms produceert dat onzin β€” een **hallucinatie**. + +### Wanneer hallucineert AI vaak? + +- **Specifieke feiten** β€” namen, datums, URLs, getallen +- **Onbekende onderwerpen** β€” over jouw startup, jouw studie, jouw stad +- **Recente gebeurtenissen** β€” model kent geen data na zijn training cutoff +- **Citaten en bronnen** β€” verzint vrolijk referenties die niet bestaan + +### Wat is een hallucinatie precies? + +Niet een "fout" in technische zin. Het model voorspelt wat plausibel klinkt op basis van patronen. Als het waarschijnlijke antwoord toevallig waar is, lijkt het slim. Als het niet waar is, lijkt het slim maar is het fout. + +### Voorbeeld + +> "Wie heeft de eerste Tony's Chocolonely reep gemaakt?" + +Een model kan met groot zelfvertrouwen een naam noemen die volledig verzonnen is. Het patroon "wie maakte X bedrijf" + "Tony's" voorspelt iets dat klinkt als een naam β€” maar dat hoeft de waarheid niet te zijn. + +### Wat doe je ertegen? + +- **Verifieer feiten** β€” vooral bij namen, cijfers, datums +- **Vraag om bronnen** β€” en check of die echt bestaan +- **Gebruik AI met search** β€” Perplexity, ChatGPT met web search, Claude met web search +- **Wees alert** β€” als iets tΓ© perfect klinkt, dubbelcheck + +--- + +## 5. Het AI-landschap β€” providers en modellen + +### Grote spelers (2025) + +| Provider | Modellen (2025) | Sterke punten | +|----------|-----------------|---------------| +| **OpenAI** | GPT-5, GPT-5-mini, o3 (reasoning) | All-round, populairste API | +| **Anthropic** | Claude Opus 4.6, Sonnet 4.6, Haiku 4.5 | Beste voor code + lange context | +| **Google** | Gemini 2.5 Pro, Flash | Multimodaal, lange context (2M tokens) | +| **Meta** | Llama 4 | Open-source, gratis te draaien | +| **Mistral** | Mistral Large 3 | Europese provider, snel | + +### Gratis vs betaalde tier + +| Tier | Wat krijg je | Geschikt voor | +|------|--------------|---------------| +| **Gratis ChatGPT** | GPT-5 (rate-limited), beeld/voice | Dagelijks gebruik, leren | +| **ChatGPT Plus ($20)** | GPT-5 onbeperkt, o3 reasoning, voice mode, agent | Studenten, professionals | +| **Claude.ai gratis** | Sonnet, beperkte usage | Code-vragen, schrijven | +| **Claude Pro ($20)** | Opus + Sonnet onbeperkt, projects, MCP | Developers | +| **Gemini gratis** | Flash, 2.5 Pro (lim.) | Quick lookups, multimodal | + +Voor deze leerlijn raad ik aan: **ChatGPT Plus** (voor algemene workflows) + **Claude Pro** (voor code-werk). Ofwel gewoon één Pro-abonnement van je voorkeur. + +--- + +## 6. ChatGPT effectief gebruiken + +Voorbij "type vraag, krijg antwoord". Een paar features die echt verschil maken. + +### Model kiezen + +ChatGPT toont verschillende modellen in het dropdown menu: + +- **GPT-5** β€” default, snel, voor 90% van de taken +- **GPT-5 mini** β€” sneller maar minder slim +- **o3** β€” "reasoning" model β€” denkt eerst stap-voor-stap. Trager maar veel beter voor wiskunde, complexe code, planning +- **GPT-4o** β€” multimodaal (beeld + voice) + +Vuistregel: complex probleem β†’ o3 of GPT-5. Quick vraag β†’ GPT-5 mini. + +### Tijdelijke chat + +Voor gevoelige vragen of testen: **"Tijdelijke chat"** modus. Geen geschiedenis, geen training-data. Verdwijnt na sluiten. + +### Custom Instructions + +Settings β†’ Custom Instructions. Hier vertel je ChatGPT: +- Wie je bent (developer student in NL, leert AI Developer leerlijn) +- Hoe je antwoorden wilt (in NL, code in TypeScript, niet tΓ© lang) + +Geldt voor alle nieuwe chats. Scheelt elke keer herhalen. + +### Projects + +ChatGPT Projects = persistent context voor lange projecten. Maak een Project, upload files, geef context-instructies. Alle chats in dat project hebben die context. + +### Voice mode + +Vooral op telefoon: praat tegen ChatGPT. Heel snel voor brainstormen tijdens lopen, koken, autorijden. + +### Bestanden uploaden + +PDF, Word, Excel, code-files, screenshots. AI leest mee en kan vragen beantwoorden over de inhoud. Voor school-handleidingen, code-bestanden, screenshots van foutmeldingen β€” werkt geweldig. + +--- + +## 7. Prompt engineering basics + +Een prompt is wat je tegen de AI zegt. Hoe je 't zegt bepaalt voor 80% wat je terugkrijgt. + +### Slechte prompt vs goede prompt + +**Slecht:** +> "Maak een website voor mij" + +**Goed:** +> "Maak een landingspagina voor een fictief koffiemerk genaamd 'Polderbean'. De pagina moet bevatten: hero met tagline en CTA-button, drie product-cards (espresso, latte, cold brew), testimonials carousel met 3 reviews, FAQ accordion met 4 items. Stijl: warm, minimalistisch, kleuren bruin/cream/donker oranje. Gebruik Tailwind CSS en Shadcn-componenten. Geef alles in één React component." + +Het verschil: specificiteit. Tech stack, kleuren, secties, stijl, output-format. + +### Het prompt-framework + +Robuust template voor consistente output: + +1. **Rol** β€” "Je bent een Senior Frontend Developer met expertise in Next.js" +2. **Context** β€” "Ik bouw een coffee-merk landing page voor een studieproject" +3. **Taak** β€” "Genereer een Tailwind component voor de hero-sectie" +4. **Constraints** β€” "Gebruik Shadcn Button + Card. Geen externe libs. Max 80 regels." +5. **Output-formaat** β€” "Geef in TypeScript + Tailwind. Code-only, geen uitleg." +6. **Voorbeelden** (optioneel) β€” "Zoals deze: ..." + +Niet alles is altijd nodig β€” maar hoe complexer je vraag, hoe meer onderdelen. + +### Iteratie + +Eerste antwoord is zelden de eindstaat. Iteratie patterns: + +- "Maak de hero groter en voeg een achtergrondafbeelding toe" +- "Vervang de testimonials carousel met een statische grid" +- "Refactor naar een component-bestand per sectie" + +Behandel AI als een collega β€” geef feedback, vraag verbeteringen. + +### Tips uit de praktijk + +- **Geef constraints aan** β€” geen library X, max Y regels, in TypeScript +- **Vraag om uitleg** als je iets niet snapt +- **Vraag om alternatieven** β€” "geef 3 versies van deze sectie" +- **Geef voorbeelden** van wat je wilt (positief) en wat je niet wilt (negatief) + +--- + +## 8. v0.dev β€” van prompt naar UI + +**v0.dev** (van Vercel) is een AI-tool die UI-componenten genereert in React + Tailwind + Shadcn. + +### Hoe werkt het + +1. Ga naar v0.dev (gratis tier beschikbaar) +2. Type een prompt (of plak vanuit ChatGPT) +3. v0 genereert een werkende component, live preview +4. Itereer ("maak het oranje", "voeg buttons toe", "fix de mobile view") +5. Copy de code, of deploy direct naar Vercel + +### Wat maakt v0 sterk + +- **Stack-aware** β€” output is direct werkende code in moderne stack +- **Visueel + code samen** β€” zie live wat je krijgt, niet alleen tekst +- **Component library** β€” Shadcn is industry-standard +- **Deploy in 1 klik** β€” vanuit v0 direct live op Vercel-subdomain + +### Wanneer NIET v0 + +- Logica-zware features (forms met validatie, complex state) β†’ liever Cursor of zelf +- Backend / API routes β†’ niet v0's terrein +- Custom design systems β†’ v0 leunt sterk op Shadcn + +Voor landing pages, marketing sites, snelle prototypes: v0 is goud waard. + +--- + +## 9. De slimme workflow β€” schets naar website + +Voor de lesopdracht gebruiken we deze workflow. Het is een blauwdruk die je in elke creatieve flow kunt toepassen. + +``` +1. TEKEN je idee op papier + ↓ +2. Maak een FOTO van je tekening + ↓ +3. Upload naar ChatGPT + beschrijf je wensen + ↓ +4. ChatGPT maakt een gedetailleerde PROMPT + ↓ +5. Plak die prompt in v0.dev β†’ Website! + ↓ +6. DEPLOY naar Vercel +``` + +### Waarom dit werkt + +- **Tekenen** dwingt je tot duidelijkheid β€” wat staat waar, wat is groot, wat klein +- **Foto + ChatGPT** = AI snapt je intentie inclusief layout +- **ChatGPT genereert prompt** = beter dan zelf typen (ChatGPT kent v0's voorkeuren) +- **v0.dev** = de visuele uitvoering +- **Vercel** = live op het web in 1 minuut + +### Tips voor de tekening + +- Werk groot β€” A4 met dikke marker +- Geef labels (HERO, FEATURES, CTA) +- Schets minstens 3 secties +- Hoeft niet mooi! Hoeft niet to scale! + +### Tips voor de ChatGPT-stap + +Schrijf erbij: +- Wat is het product / merk +- Doelgroep +- Sfeer (modern, speels, premium, donker, ...) +- Tech: "Genereer een v0.dev-prompt" + +ChatGPT zet jouw tekening + beschrijving om in een gedetailleerde v0.dev-prompt. Plak die in v0, voilΓ . + +--- + +## 10. Ethiek en valkuilen + +### Auteursrecht + +AI-modellen zijn getraind op het hele internet β€” inclusief auteursrechtelijk beschermd materiaal. Wat betekent dat voor wat je maakt? + +- **Output is doorgaans van jou** β€” je hebt de prompt geschreven, je beslist wat te gebruiken +- **Geen plagiaat-claim 100% veilig** β€” als output sterk lijkt op bestaand werk, kan dat een probleem zijn +- **Vermijd** β€” namaak van bestaande logo's, specifieke artist-stijl waar de artist nog leeft, bekende karakters + +In Nederland: rechtspraak is nog in ontwikkeling. Wees voorzichtig met commerciΓ«le uitingen. + +### Bias + +Trainingsdata weerspiegelt de wereld zoals 'ie is β€” inclusief bias. AI-output kan: +- Stereotypen versterken +- Onevenredig veel mannen / blanke mensen tonen in beelden +- Minder accuraat zijn voor underrepresented dialecten + +Je kunt actief tegen-prompten. Maar wees bewust dat het er zit. + +### Transparantie + +Vermeld dat je AI hebt gebruikt: +- In school-opdrachten (waar relevant per opdracht-eis) +- In professionele contexten waar dit verwacht wordt +- In creatieve output als de stijl AI-typisch is + +Heimelijk AI-werk presenteren als 100% eigen werk = academisch + ethisch problematisch. + +### Privacy + +Wat je in ChatGPT typt kan gebruikt worden voor verbetering van het model (default in gratis tier). Voor gevoelige info: +- Gebruik Tijdelijke Chat +- Of: privacy-vriendelijke alternatieven (Claude β€” opt-out by default, of lokale modellen) +- Nooit klant-data, medische gegevens, paspoortnummers, wachtwoorden + +--- + +## Bronnen + +- **OpenAI Tokenizer:** https://platform.openai.com/tokenizer +- **ChatGPT:** https://chat.openai.com +- **Claude:** https://claude.ai +- **v0.dev:** https://v0.dev +- **Vercel:** https://vercel.com +- **Lex Fridman podcast β€” Sam Altman:** uitleg over hoe LLMs werken +- **3Blue1Brown β€” Neural Networks playlist** (YouTube): visuele uitleg van neural nets +- **Karpathy β€” "Let's build GPT" (YouTube):** programmeer een mini-GPT from scratch diff --git a/Les01-Introductie-AI/Les01-Lesstof.pdf b/Les01-Introductie-AI/Les01-Lesstof.pdf new file mode 100644 index 0000000..2228195 --- /dev/null +++ b/Les01-Introductie-AI/Les01-Lesstof.pdf @@ -0,0 +1,238 @@ +%PDF-1.4 +%“Œ‹ž ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 9 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 +<< +/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +6 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 +7 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 +8 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 +9 0 obj +<< +/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font +>> +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 (NOVI Hogeschool Utrecht) /CreationDate (D:20260603151533+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260603151533+00'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (\(unspecified\)) /Title (Lesstof) /Trapped /False +>> +endobj +17 0 obj +<< +/Count 9 /Kids [ 4 0 R 6 0 R 7 0 R 8 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 2040 +>> +stream +GauHL>Bed\&:WeDbiq;Jkshg$hrK4Op9Mq'l*fErmYuX65_+KlW@d%MJ,PBY,9$muTpuQP,aLnUqS'LRA-N`4XSO!`!IjISo/@'pL]/-XkBP@gJ_0kWf:Kk^*dU"0Y+T[N_22Fq"3%(&JKHj;`$5J8'L&Qa%]9X$pkT>r1^(JJ_\N-lG0n"dE/i4=3[$[ooAKbPUQLXSN7[($U!bF@YpL->FLR-RT+B6nDN(-$3Q,-]ca1uYp&/!"'q64oVi!ag;]RbeU-gS<FZ6LMU>4Y.&fVH8YKc?`BT(kH7C@A\$nED7sI)5$cC/^aB-JS(8^GGX3aX:Gkbf!PpX="+N+U#bt9SC8^)Hp?Z6-$Vj>&`docS>Tau0+QMMLR`[nYPQH@@t&.jMt-4<8D!3lTrd*GW2_]thH=r]b7QnK.7IZe5>(>)L"D-SF*;?IH;\*"ZR1JQ)8-g^G(7<&Is;iBXm3[;0jiP17+0K:5n["luX#q3lPU'Mo%]qI_fP@aKFGb3>#*J+1=b4nS0Xk6mq3\P+*1g_1C0YsS8%0pZo>:)sW+sJe*`%3%#"b[_$_alB78]uDU+*n&A-aN;H9bTP9'lXncZXm%$5XpU3AotSi1\X-tGTYmL3'9$Am!An<`3$iLG-]d+Z"mRSek4P/!\$l%ZL%LQPbW0//,G6FoPiCsFW4\2"@hg)G["c;\B@1_C3,83ViR%:up_/qQ?53a@lFos0dc%q&"a&ih5C2sEUj:/Q4kVf&@T1p)Eh[o-S4N6C15m5uncGH`cm+$HTjKVNbB_N9^<3:?(A@4IkD.N*#DVa=quP1Dendstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2105 +>> +stream +Gb!;d>BALX'RnB330P`<-EcGlRG!?RQ_lR7S$m/-Kbo:N7)W3//481Vlu1c+)1rSF+d_FgRkt[#lWj+2h[osW^6njZQiOeb_OGTNb_,/mD)kKn"2b`MnD"T_=82jDgUELIIe!!BJ5,pEh^"oArsO-PJdJ)$$29*SUa5V`A*9:+Uh`F0lZ-nNKX@4KS<89XZc,7Olqkk^^6d7IJ2PV"'&_(i&5B'jC`Z]FOd/*P#f+2gD7p$2&V9IjNX`/6]U$$\U]_qi.35\Eb;&qoG%,G")5\&7c8go?@^:e4cMaGTq[E0.*M4B4Mf\dESE6YMP,u,)a!G.l%m+O[*&_J#3[KbTo!ql-kYc)m/SshJuFs/P'^,brQ4`+2'4Vl)I>\lIc1FH]a@Gk._DXGIWZXL8dC0S__I&djd:(aDN6*>6jO5Mm;3gYOX[L0,?]p@h:mjKi9#?Bo,//?Km?M,;g%H]?1%24k`\Ou+q.9E$'5ufm`.9"GGbj,gDqaJf.;p=u%F.Rs(@3BWNhmB^*jek2f7UB@5f56bJ&^eUOZJ,]aRqiicb]o_"4p%;jVOp]03*2%;3k=DLrDiS':,6.QGR0W[*Qq@Bpq9l#6YPM*.M,LRQskY..]rrX8$V3)'RNRC=k*t*?ak5L@"V$e0^ieh1ScFBYjFB=q4;bGH>u)f<'!*Ll1r?o^`=V4O!!fh<&s]!'`6GZ3fnl6K<05%6uuGR.81+fT1tM`rd^@;69c)H=i#F@3sJVosEVUO=Tg"0QVhQ,&t5qYBY^K1iET#uR;t2W6:lS(cHiLA-9of+_p>>rEb6pG`bqf]l'1Ie)gaX_g3VhKPh-2og:A[aXIQAY(cQjUf.>hBuPk'uOH_#Y?UqSsA4$ab&^T6AfcTP_Bc3PFId*?KdGd';,U?m`g=0U!AO)SIkQ8@"KdO@YIXE\=2g2Pr?0&VD!*gOXg*7\-0R'U4hcj22Sla%`=iTV10BP)3\/Hn/kn[s)f(ehN*fKnYP(W7]uHJ5+6R'skY(AijIa->$!]I>V=e;XhFW$s-m>Jm(prQl=D"b"#Rq,M\LoRgi*`&EU`%QoUBtUX>:+#tIWok@um.4H/$DR,!;T8'/kG'%MuY^`PCUd3ngeJ9+TS_o/Km.;p=\96*oJ<$K8c=@Y!YL6PM2m"@I7n%KY,FH(])?(0`&PV6l#7I6dUM>09Rf'LK+(F9@Z$[mu]63B*uCN-DA_^iUl8c\sO"@eG>pbSnjgXC/@m:r8-55(WhZO95_0FUEeB!6qV=mAU]-'5'JX)2dG0d7Ze4X)W1a>]t3JHdKBspjb*c]3F^_*K:D^\LDJ=J:/eN"/Z,6,]NGYJ.P6P!2.!\j5!i[TAt!dhCrK_'E)L_r`_d$rOV_2d""gaM\40)LVqN,b.1nrkqF8`3@PkOrBfRk5m/WCmCHG,GD3s65nWsWT/&coA/gT.T\qNbaG:<*k*DT[rKON>lkCL4?F1#9mXDE3a@Ipb@ho<4:@EaG9@\j$HPq:aZ&+5G^HKEm"=;8?Z).T4",NfqYUDkE-S*IS*s%:BF;=)6^FFPb;k@E$qrn-/1]jIeaqR$@hR\/,o5O3@F5)DI&T(NS:BWIro.dqpu;'g5L>b$q$aU5,d4GJ>gAK%GA$6%m?8"+X$`q+;q>/.>Nf>?KC%"u=9U/1!3XY+`HK7k@Zg:t6+WNG8?G9VgUrr?\])]S~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2327 +>> +stream +Gb!#\>Ar7S'Roe[3&"1Q/Q.>79*'cUCNh4hG4"G"_iP[;VJX>aGS9C[IFrJq8*]@bb,$&g`K"J,ijk]M*7?$kO(GIc7eFrq:;=-VZ>%#sb@Neshg+-@mfj%L65)OGN#8tu[WEjXiHFcaZ[49=X6%M.a/1Il'Xjk(tE'-enkB.>I!g@UrGh;khA+nmbe$Y^$<:&5'U:'?hN:UVCjbQI=Qmn0bZlSk:\9JAS77i9EPhIf%#RAZ`(_:keIDIIP76V?o]@T<@'6o+fB=R]TH-K!Q"PjJWon.?qYojG[O*ON[pUlXqmtF=fk[\P]pY,7(PK@EGOA:;j9KE'sTu^aK6G((86"!26Z7XfY5@Uc7L*juSYO^LUS^I'U!]WZH91?f0d4i6SQRSK.Vg$U1Zs@O\,IWF"mBe&1jM&Sk4K\.g>muZ,l-q8-mp4`*S$a_P[;2>n[C:q*'6U#KBQ/D,DlpuFlq:>_]ZrY2o)q"[N/*3Gh57F@'].)c<(as\QT:b0ki@g)OP)gm<9.gWd0M&-l63]R=[NAl&X:$YEWn:t.g]n"\t?GN%=-iAE$p=Rei&)j[,'n*DAki]qj$/#f8/(p2+6.4H^Kb&4oeC.F]=h^,OL,CJ;n!UlVO2.e>F*p2K%#Y'Mi!+2$,=p&)l$Jt7/cC__f)<;;GkIe4s`U#84j0UVebnh,3FBb_[H1^Mq=.=\'?$fn=de-$I,D>:D!g0A$4Tc^R9kfsg9&Hc=B@QY(*%Ne49=31i]e/2NRkgC<(<6*/'kCXF9jS.*"YFne(uWU$\#'S-$WXLL2q\I8`i>SakH[E!R?+eS>rR(`'hdA3i$K9r/ZZ)r.$j`u#ZO=]m3;QYS$)Lo^3_Xin&(j12sDG'rb3DN&-F9kVl`KZ"ZZ;Xms]D/eZ`n7+Nbq/L@.Jp(Ug]``iHOr>O;=$'8]scCnI.e\e/i"_0MR8('ng8B@R=eo$]V%CS>j4+5\(;2g+_S(AKUkkfk>-U;3V<''4#T$>+'u=mW>6G*QgW4)%33Ugc9=QiFDWqFHPB,SFpj7GA0->ZTCB8a9('CqeL>"Z/1^>&"IDAW4csY:8/*@^Z&>(#_[:jFm`.69fnP;!PK!`#a^GTVA/J=_L's;QS7AkGH12\CQIrh#N$P^6ppT72e8;Ih<)(aiZmB&>Ji+:BDucnZ\A"5D&%&QD(I/2gU3;Z8hoQl5Db7gAiO_9jUmN(p?,/W<91Q>e3`\g7do7j?a)V.eTA@>AI2)=kC#dXPat%"2@S]h1,(9m't+K2C,06Tn*g]fKcT6*J@Qj..*%cW`3RNXkIb>Ds9/Gc^P6CeH>*+jgb>Z9A`+)"e)N4h!&QV<&Z:%c1]7GXL8#e74JkKM4u8\4+4Dm<>i>u?_>AX8hl-hml>\J4=57LIcsYW%j69](jR\%h4=nd*H_hH=GpD;:c)FG-!XA@E:`Rd2_WZ^DWFK)6Q_ms5'[2D^S)$nGSG1boVmEIF0hf0s3e9l?H2.FeE>?mY46I`ok@_1oE0+D)r6e4l(TEMSasG7s25uG`MQ1`'X=2-$%^#)ctXKHYGNP+6'K%aCs))O~>endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2149 +>> +stream +Gb"/'a`8=Z&A@B[\l>YdS.$dO0=dkma,upu@l\.eK.\:E8O&:9rqXs3OJlJ;0*>3(`\Yr[kk._TUc277T&$*stIhr,ttde1bo+5>G]*YMe]t[su&BQYMQBFJ4qLhi5Y?_SBFgUi?8>I_lO$[7:*'`qM1d`X]3d1eKNl6B^aAI>qfPe>rssbN<%bLirj4,6kac8^P=H^!+`10-]OoJt`"nT7dGX@#(\I`G:dY:Aa;[8m&%o"Y:O>63D;2P_"lDAfK5hX(k:%OC"*T'hF$/qHRumN=l:#4pPis;+X??H0N1=MQpdC;+\:QEpW:&XVb7\KYLoHL$&-99DB8Ta%CgU]"FhH8sa<-"ur%a$>*Zp\6Kq[U3mflAqgBO]#@"pbWp``V'cZ3"\@nfk$cJpr*Hs]$m+Laah1mhr?`s(h[_/q)Ff@J94B`Y)?#bL'ain@Jb[,Car^fgoXU63_%$IKrc^0RcADf(2X:-O'VSb*_2U=&GrH-!r%oDs)>\7;qjH?&of-=mjrs(d(;eTDW;2AsC7$/'.-lF*VKLE?W_EI?@)DAHV('A'Qr8s;/fhRReZ'V-QU.!e3>/p.5*lDsfGrBJ-G&%`a%;p\T.Y)o";1aI`G.u4?tgIAT9t,dIC]/t@t!M;DfDF$^9l=%7A`JMVi0R\gnJ*_r.gaP%Qn)PF)sFGqeU9#p7pf&ioa+UP,+*Pj#AJg"`%[+G@Bh?)9WGdSOV$CW?r8mFm_@YphiXqi]M8!.#BP6isk"LDf@mc8P`Z0YX(W_gc34EX-$eq+P1/iAnGh\2S>]B%\4=@*)?24Cn\=rm7EIhUD4Z1[3#e:mOI^Xq!QgLeKuKcj%%gPd7ut`,$)9Y`dW,KBQkJ2j6!ekTiiDK\%V@nADf<>%rnB2ADH+lF/o0h3:sDF'hRV'IgGR.>=/YPrNCh&XYlj[RBZ29A5_T>I,:f&?Edi<=D5:qj7T>Yp_L]*u=+YEt;YFGS%RN7K`kEh,\/,C*T!8YZk&M6CdnM$enUV`'[@#jja/]0eb#^DjtgaP)sGQN$>(+m'rnAdd3jhT2oQ&?:ugTGHE@5nQ)A&?p>B9VZk(pub7%]rY2[+4IrB\3WmD9LO_XnE/#A*DXd2VU]tWq6uGWmR6nj95emeJ3HbPP6cm%4#6u.]uO>g1q)"R3i!P'B#0ibj&tpMKjOMLe6*Q?UlkbLhno48Q]Z1&D`8K>B%pVrR1:G[hc>M;.3F3MLi"#DnLHpss1ipN4%$&>8MDPR"!j@n8u5I!3UI_RFBhA"nDCoE-^U=9+Mm(O4"XbXUt]HPriFH$L9OqXW(eQ/R.EkTLR:h-]049l.\*0C.es*/J'MQC;Lu9BpL=pI@]ldNLX#%HgA=$[(:>f0_i36dAg.[Y_TAUe!4-+"=QqWEi-_ioR%/,_B]&+p4S:Cs\GaYRcKmicj/c2#Et4gF<^Slik8GASEj]\#YIQ:1LG_q>08?"JTpR[TX"H#1YSXW]HdOi@Ah&f%_hHHlFt*6NJ/'<9LYTbc^B=eOlM~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1990 +>> +stream +Gau`Tfl#P6'Rf^W>SmaHV@pdp1!k]W:4OU4@+BPmbD7-8A!%ar*(jqZ^1XssZ7S01!]L]bNQAZR\`YS4MbO&XR_Ni;oO^_!5$2@K1nBil)D4`Ke,/CG56lr1gO\[QiIn'AfjmL2r^UOG:lsiZIUkqaKk=_GJP0lo).=A9A7JsX=94\Wjt@5r1'6>A6C3YXo)-kLZ/79`gP0Ipn9dI3^;%!-eE02ShcU1?gCJtr$*/,^/(JVKe+*R$VE'XMrcRpk50B*knNtBWr`1>+(Q-LT4.t2tkT+%%9WiX94&%AAqGJir&aK39>k!BH69k(A;KCV$3FSm8(jS1E^&c&VJo>jF"cdGQT'6<%%C"@aHMr0?-^.f;nC7c';%oa%K/!-G';!TLn=RA'U3F@al\]:BZh`CN$@FO34uB:r/sn[`R?E`=:pf@2J.u/+MhG$3c+mBn"YI6YUm.oJD@4O3;'4V/fJZW`rJk;uF1\hp`sd;fb'nh2J_Bp@.nsqJES%U>.gU#Md]j5,Z9i?-l__Hubb`r4A8)1j-7pStI2TGO8beh+R2T"(n8(d6.8,QB(URcXmZCu%4/*XVB0W$[]rm,M:UqmlIK[%<))!FOXON:Wpg.MU^Vaj-I6Ok%I3I4ApC"I,ZesTVP-[arU.>gLSEY;q//<0#PbLdTeO23%U;%R&/Qpd0[-$!qisP+,.iV>;CLB'g4W23o?Y0-?V\mk&[)uLE[G:lH1NM^<'QW;a4EG%o!0E-?ajCOd,kM>G&MZNMrJ!VqDQA]0krua4T%0Q%01gG46ll34.ci5A4K$J@pDR;Fk?i;/]0k!ioTLV=%/O"_GY.4aZ1S$Y/lU*kF_C1Fs2fT8<*Xt,iaN30U1Xm2md=-U4*&U"a,`$I#q94SkY/NV.n(ZaLl,8k(IpW/4h4%;f*jdS_(@@@XpdAR&C_*sJg3]Q29#tfWKIRY^TB5NjRfDO,O8G<=]E-o^l9DOr_os6TW%;)cTS]ToMOi+`_2"PGJU8J3"O`g:Bf3lcBHdi_?a?1HCR'IujVKtnP*c5k4ToYC)_h8"12LZcQ0>**L21W*aumAMs\SH6E"irs`C>().ZZUAad/XHj2%4W'p)*V2s3%oCO2XWpYl5N%-^0:i4T^)!oURaf-J]7N7,q#"8(<.+J`n9R#^57r@m8Q*8Z^+!Ws3qlUKPr#c1T5Q[0]k__KV7%McS!FiVrOZ`$DrQ"!E[\^VErj*#KV!-i\ZFR4F.cHZc&#[0\+3W%P0@sEClm_bF+U\Kf4rJt&&eo]<-FA/dq&j393;Wg"H0Zl!pq4)MXM+\iN\tNZSGA3..tc/\2E]\t/s(+J/)T@4s'nn\CrpG>YB+0b\1"2ha*^C=`F'$"lu%1Jf_q$IkA84A)]#h*M-.M31>9bmr">taUN([lq%Wmr[Ng)qKa@bR&3:_:R;4B9I[[eK]sRPc;!_HEY7=Y/=]^h[9WQuqJ1[pR%hPperDr`En]Wb46hOp.&T(DSLEn?l4cV>'dOta..jnC?7eBnQP;4[5_tP.afR:Bqpm4WE]j(QIChMiIJaZnP_.P8=hdc#l-0lg7>.Q;_Q5&B=iO7E&d/GmA4(DI@pQni!]\l5!_\XOQUCbtqLht9+,@'"$B\n*)N?,]l'.hkhS:ej_^i)APQ9*fEeIA-N323cZcGdc]n-LO[.-(.7ilj.@H(?[3n/kNqJoL~>endstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2310 +>> +stream +Gau`U?$"K9&q/*0kb;g-"4nORg*RkmRNLq]ccVeP[mXQAc7Qs4P/mf4Yf^+(FKdu]eo(e#mFJ]9saKKo:7"`@n`Erh)+O!rV:#PR1$2?Us%s5B(LYkE(@9/C-?;Z$,WXqldDH#ds$:h^7\j+R-XZRl]OG7_tg^:X#TOpf`h>6B6QK(m,SEKkU`"N]Wo:IE-3AAtc`A=HfG90%.Qi2MTs?J6M?;Mc&4hsIq;D598Xk-ReWjs99)LK8'd#T4nW62J]8F<'Ig(*DXY6l^r^7m].4NfPeFLW^X*hk?)eVec/'$W8?:Yq#2''(j(jL4"W&N5b8GZ`,6X"/1cc'k811c'!i'[Us*!D2,b_j)nq9E/bM)XiE1^IJTSJ"VFWN]&hWa`Y\F7ZepJ(&@8*/W'[M(4`/bhY-mfWO(<,SO(b7X,KU[i$&r'o;&/`pHX.D'L+Lj#!Qp1`cW6M%WWZ(P4;ZSSH:?G%1f\@CcFB*r$)=+'cJ$1([9Bu:0G6_i?-I?Dc@m]^\iSr!C/ABNK`Ei$MfR$56c4h_k:NuE>?PYd79Qg,`1qWZF6VjSTCQ7XU2JAJZO\>P>an&pjY%r+/Q['[QWDt-Y@o(QeY5Yc=c$$cBQQk)-7-tHX%c&q-Ip;pYc(kD>#!Y22F?5=5]>=]SBsOBEZn9ckEhMfc-'m"jJnd:C8r"!^u=\bWhM&T-<1"FV]30rME\>e2t3pk$(fu;.b@`DQCk3X:?,?(FgA.Kg0#*2K-g%i!N3X=D$V1M$km-*VEn[J3Z[X\=JWm"$93)\h]eS5`8[/jRhlqKd,IAc/Q?gN[2RoaTpVd?&lmSgNH_?NB#0OJLo@E=0k$gYE4^3/=Ept`'gCs(>Wu^*gL8erM.jUnbDpXQ+TopH#Q-LGLUkbX!J10f8j=Ml?bY?)mRU*71K1<[`Ct]=Q4U+>q'I],-TL]PFOP1TrQ*#6SdV)CN1%a2#.K+nETqG_''\^R4J`)i7#dO84gqmqq(WTe3&'2ltSbOVsFSA$J4q*uH[Zj8Jl67mgi/>F<.:;4,:+agqPtjU-]&:"tjd-aA%D\E(>OqPd>`JkY\d'dAU%0'a0P#ns;thFqePckUK?I?BQN6t[_`jc.ro-un"A^`j8b*<[j.-=J0+Ca;".4jqPV2%,M$lskkZY=]mZMM9oJ`F),K.+R^TSUfBE\cIE!]21Fe)=fj#N),oTT?hlnN;N%IW3l=>Xc?_c6,2Gd#FbNg7%>!\2?@;V%V/-B^8&R5p1F=`C"&%b6Ab^+\'1OS>9?eW*[F$pIflif@(B4b[rJh$?G%\+46X-;.CGrhr6?JOi=)0J&eW$9DTloY\f!c/&rQf@9Ru/p(/Xf0p-O>].CcnMX/4&iQ_7gY;%aS5J&&I"AN-o]Wl-0DX=_:^9/=L/X7\B\,.(!C(p9M(TYORoOtYe'D".A*2bUE9gF3L+cJ2/\TRq1gI"IWhJl=.AuaE&s)2Q,A?bsS7,A>Y>/iq&,@aEW-hfnr+cuQHNQ>&p1buPr(*\JU`JTY3)#\*6?`6j2LEM.,U\K'$4iloqVo2D[Mendstream +endobj +24 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2154 +>> +stream +Gb!;d>?BQK&:XAWR.=.<_FF8mcfme`8])t`UiFK?3P;GqYur>Fl=sl8IXPk:Q>F?[0Eom4N\;nsp7'agDn]?n/:-!oqeuW+V@Tleg0d"h++`f6&,O=cf^Jdf@nts8Se'&)/2qa&`NK1B3UZ"e#:t\%fS7HS=VQ2?75g'u3!+l,$UZuYM(j8/Z[mg]'h07KaS(67\F[AV!lq"64luu8I/E_"^.?/"=YN?90[2g"UH@hQ;k6q?A!ZM;54g5,qr-W&#i6;+XG.N[OhkZVEHn28gi5Z*I0@=F'XON%q#^DZd!r#B=>K.F&\/I+2iSkgnoH#!Pofn%]9>MabAA;<6uIeQb!R1i$JVCZ':bcorP&kF+n!XN;)a$TsF=eQ\rZfrXJAbAI;bL?Mc]h\J#!q-RXc^1hn._&.6XLf+7_%Ps=ujpAo/R$:%"\_;[09G'mR;h;9_819*3`EDXNc8lJWp..'$_YNWFK_#m1:dH7MIYg.5YVRkM%"h`_,-O6Hc.GR7"cC9p(l/SdqnI4O@RCDB,m+'/,4Mf:*#.KL1i&T=*mcN1YfmFm'""tZjIAk&6TN*O%YBbNO9A=j2sB"*D_TpjHM_csk?`coC@HZ1$M;IhTY.@>[F2IFkk:kqGn8Z!Ide24F`)un>*,o3I+s\JDQ^SUNL>LRV%n^?:Po\3I6Q[(RlM6*m2?$VC/rgl7+EpKde9LrO]1Nl.!)1tP,V0D)a.Obg:=YN`gNc?`!h)Lc;f7DqWg9'9VC.G!+)+9lp`k8o8#nlhLV1cF%4K%U+`:B*XUCX4>VmFMX/LY*2978R3cj[85J;-X8!fr-q31+6a6L?52LkkOts&O;&?\o(t57:YbPdN0i_=,^hX5lk@HYe"%9[c*Z,*n#:WLFV%/Kn1juXD'=1,u(WqE^_6?FZ8**jb%I4S!MDUI`78W32eSeR8C.@A3.kjR3?H_qG:0]PKn-?IJc!J2th,s>8(P,@9o*h=_(VLAKn[&2TIe=[7He4Lot#:?nut^NV(Y6@Z^,+QH2+`14rbN:g(Xd,on=0o+ffG`jM%OW-)U:RZ*D2($>6m(5MSa"h7,GAm9Eu:l!nURb6p9]gjY;F*nme@FFbacuf$<=P!'f90]p'lAbiGApIFe4;,'r%"$ftm1U,i[i89%QqN8I\Bl/'IG`i0+m;D:eDa!LZWS,69:)c70l&MqO@ifcnA/:9SNjmYB(:#r]BD+B)>_l!8JgAuaf2&-=37;gLfs)s:-]ON@W<5ARrR%lcp.I"n3aO`#r_(BjJ>t&nB`&X6Y;$Qg_^(`f+H=<&=Rm]q5%4uX>,]YUeMV&=oVKhuXj?3rKaq<8"L_@(;QbdM)$km.'LLLg6P_k(:C@D3V$&2VXj,3NZ*q]XFUmDc`X3hW?3eR9M/Jr0rl9M"0%e%\Z7TCt32p3[Xc@IuC4?'g8Jiu%)GK@>bu189(K.?u6UWPoL?DO<4^4m+GF0a.F'B)`i["+:IM1_-sdg3>l*j7)^9jR9Kh(98k5E]XX,n%,&_/LuKg\^rd'ST)'?E,#"tKR;oGT>kX/g9MIZCRSDRN$gF@Su)T;706%Tm$W^RoE%HjMS/B'8\ifeQ7%%(D_5(%;(2#dbr,sV+5djr/a5!!\2f8u$."J=38(4#&8Znm\4^u.q9_J]Na>(WG+r6#!FKT@(=^MOo/Pt:Tk23oog\)%o_:1D,D3&9#R68T>F0%/0H+bEnfCiendstream +endobj +25 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1902 +>> +stream +Gb!SlgJZcs'R]'o:/\J7Ou5RhF`Ult8-4IH,imrq?[*G]a(B-jiuO-G?+tc7,TCR8NaYjc#e7BHd`grsl0bfH0##$qB0%AceYW3hO$'&GII[FeT^Fm&m-aebj7n`Y#9c+p`,0^GE]S(77=)1::`)2ia7"8;\?V79!C,Y9@,+4%p2(R&Ir^Y&(tM'FrS!ceo,N0q+k\XKaO:>I(9?e>gR((Hu@&>FDld5)OnB"=6\KHpS$UKGDI_PD!h(bi?!5?Z$G)2/qk"pm2^t4J-#8k)g*1=c1k.^rEm6j=f)cX1EZ?`UQQVb<`R"-hP'i>M>"(f:'[le]%Pt4edL.C6@/!`b[&eUCksWkg<"O=8,IgSNIn-X1dp(ndb4'C_p>543]]CeIf4ZhV+%T"#A;@A!;,*&"*h6*SH++1ce4O>Q:5Mt.&foeVab^;$.]3Q\ehb+g0^`1Y/Vea`h35aCCH&k9XY:04?.KXCA4^H^RV7sdWM+b0Q6&2,Y'Qjd".TgTen'(G.*a%/+P3<'6Js\4)f8TIVU7*+q))rp2JlKd1k7:QN%oD)!g_1j,@JVgIXNn\dQGStj/GFFS=uo+528,)3+dOl6lgZ5CsWe?]Vle1nB2W\MViNcVuR)^4emJV2'F_KP-Q/k0d\D"!t,/J+MtcenpB[.obaQ?giUIsZRHp>arEgMFR?]oA))6S'aqpR-?p7iAtCC4q1^896_-[U$>:pts.jNuK*BH`:me>+G0F3\XQ$q?<@VG7n@?Bl-m81[3P\K$WumU/3'i:cZ!'I%`,r49\KgZ,l3i!D]\[I2iEAK&4I*X7#S`r0%_^r[Hpf-B_SkhPH*3.%%.t=!\QiU.F.dE/>daUiHN2o*IajGj':,#HSW,0:%H"pN*A"nu8j\0]b\:cP_BeCYPE8BDbatf<9gQNrAfi=5EGZu.eCr[IKkfAoQZ87s.m1t\CEnd2Y-AWDE[`cMFDl2:-.W?Y44%1rc'Yc/Z`G-#WuU[H%j7-2LtV*)NVkj8nF$a'_"hcfd^K;0lj9]Y#j'4iHOqA7[r=R&)B0(VU^AO458&%KD$'B4_(5=@[pi>3ad3^lQ9pT,(I91hW2:.I9%D]-r2JT6ck]qcOFG-2ojG^+1e%TQ)lbrn6-'Hak8eEfm46#fJQCWHKt\]q?5J&n!Vf_]S1g@2O(]QIA$]6CU$$;h*YZkoa](]!ZPrd)j=lV4I:)^`$[m=U6B'Rn*'1BSZ1I3"M:W)qV,h'W!p]:]f>0TA*$km#^JgOW*hIB\Pp(',YC/Fr6O;?3?RKY$Tap[fV`>`:C=>qWcUe.9k+QdJ'MK\t$a>Gph~>endstream +endobj +26 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1271 +>> +stream +Gb!#[D0+E#&H;*)Z'1SNEGD+g6>q+G+6'YQR1dQ.WpXt`08Hoj1X_h?3^N#6cUn7tK-?p183m?6]A)0)\S(Y;*#s:rp+n,tDIU,%_h#E2:Omf9K6R[:`b0dn0fO&=cNi$^SGp;-2X!(Q0\Miq5@D=C*H6h)O@>iJ?Got\%3fb*>o<,D%)V)e)$^o]:)J8l,45\5Yd;'i!nSaM*qP"lnE'LHHr1R6J-E";.*V*'@l/hA5]*Ur4m$]JoH>i[GPgq04hT5],*<*/HPGs7ul\=oo4oBnQb]Sd)*9%3Z?T&D&N-[cX&;'DNd32PUeW\;"S]IT>0\A/MM&9K%#i&E^@6\.^ms)cu9/(J>;\0F`NJD[YS8MCHe@g&S$d@_MN+0M+TUA)[YX2pKTE:6W1u7s.:hFh(NrKM3&LeKYJAY05G1upFN?77b#i".>-S[h-[1dCIbrZejq7R#dgP/KHU)(\VSsHOAoJ2qPg7GD93mlQ+&T0EDn80m>m;N;K$]_hja7]97qU(ar]a;8_-akHBnM&`;A=kZMo8%>\3_HGk4-CbQ%r&(ICc>h,5:RVtks.E.3XXBWaYi0TX5D+2Of6jX6r)0-,Rl4KMA5YC(NkU@C!`]ie?t##&lq?1!%JquSr"FKNTI*.kD,ur+Z'.TQs6[=-!)^`]hF+p(JnMn'GmQ]b8O9Y$ReGD;SH75(Bk`R@j.cV06E\;4-=sF>8O4TotA.Z!e#6D+1dpJhbI@Z+g!nAg^_trd7QP8h+fSLY-[6^Lg9*1$'TYpCtE-/kL&^`!_E)4.QSqe4[UOq7FN!\7q=cM>$G%Z='&rclpR=pd;c1>A;MqAm!@=cM0"-_o\VViP(qLaZ6+K]'=NuE^cOMa:9T'dMmb3H#Jde.9ZR;i6go+Nf=m8u3HnS_oDLATnH0X,i,fgf^5g-VO!_^@!603[/!BiP?,r'eL\MCm0et>ZK.D)5LhbK$s"4Y'Q!=*7Q\8Pr*&Gr0!_bJgE;6,AgZJZ]s80/R8$^OWa_TsZKIp0D_Ca>.jRbm=Y*i9T`Hmrqb3i(]lM[RF\/s\or#4_'Wf^JM'i.PJX?&]UD=tYY>E0um2doendstream +endobj +xref +0 27 +0000000000 65535 f +0000000061 00000 n +0000000122 00000 n +0000000229 00000 n +0000000341 00000 n +0000000546 00000 n +0000000651 00000 n +0000000856 00000 n +0000001061 00000 n +0000001266 00000 n +0000001343 00000 n +0000001549 00000 n +0000001755 00000 n +0000001961 00000 n +0000002167 00000 n +0000002373 00000 n +0000002443 00000 n +0000002728 00000 n +0000002841 00000 n +0000004973 00000 n +0000007170 00000 n +0000009589 00000 n +0000011830 00000 n +0000013912 00000 n +0000016314 00000 n +0000018560 00000 n +0000020554 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 16 0 R +/Root 15 0 R +/Size 27 +>> +startxref +21917 +%%EOF diff --git a/Les01-Introductie-AI/Les01-Slides.pdf b/Les01-Introductie-AI/Les01-Slides.pdf new file mode 100644 index 0000000..d0a5dff Binary files /dev/null and b/Les01-Introductie-AI/Les01-Slides.pdf differ diff --git a/Les01-Introductie-AI/Les01-Slides.pptx b/Les01-Introductie-AI/Les01-Slides.pptx new file mode 100644 index 0000000..c2741ed Binary files /dev/null and b/Les01-Introductie-AI/Les01-Slides.pptx differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Docenttekst.md b/Les06-NextJS-QuickPoll-Part2 2/Les06-Docenttekst.md deleted file mode 100644 index 56a5ef1..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/Les06-Docenttekst.md +++ /dev/null @@ -1,306 +0,0 @@ -# Les 6: Next.js β€” QuickPoll Compleet - Docenttekst - -**NOVI Hogeschool | Instructeur: Tim | Duur: 180 minuten** - ---- - -## VOORBEREIDING - -**Checklist (30 minuten voor les):** -- [ ] Demo project klaarzetten: `quickpoll-demo` uitpakken, `npm install` -- [ ] `git checkout stap-0` β†’ startpunt -- [ ] Test: `git checkout bonus` β†’ volledige app draait -- [ ] Terug naar `git checkout stap-0` -- [ ] Cursor openen, terminal font minimaal 16pt -- [ ] Browser met `localhost:3000` open -- [ ] **Scherm delen:** Teams β†’ deel alleen het Cursor-venster -- [ ] Browser/notities op apart Space (voor stiekem opzoeken) -- [ ] Losse stap-zipjes als backup - -**Tempo-indicatie:** -| Blok | Tijd | Inhoud | -|------|------|--------| -| Stap 0-3 (recap) | 0:00-0:40 | Project, types, layout, homepage, GET route | -| Stap 4 | 0:40-1:00 | POST vote route | -| Stap 5 | 1:00-1:15 | Poll detail pagina | -| **PAUZE** | **1:15-1:30** | | -| Stap 6 | 1:30-2:10 | VoteForm (logica + UI) | -| Stap 7 | 2:10-2:30 | Loading, Error, Not-Found | -| Stap 8 | 2:30-2:45 | Middleware | -| Afsluiting | 2:45-3:00 | Huiswerk, vragen | - ---- - -## STAP 0-3: RECAP (0:00-0:40) - -*Dit gaat snel β€” studenten kennen het al. Doel: iedereen op hetzelfde punt krijgen.* - -### Slide 1: Titel + Plan (0:00-0:02) - -> "Welkom terug! Vorige les hebben jullie kennis gemaakt met Next.js. Vandaag bouwen we de hele QuickPoll app van scratch tot werkend." - -> "Ik code, jullie volgen mee. Stap 0-3 kennen jullie β€” dat gaat snel. Stap 4-8 is nieuw, daar nemen we de tijd voor." - ---- - -### Slide 2: Stap 0 β€” Project Aanmaken (0:02-0:07) - -*Tim opent terminal.* - -> "Stap 0: `npx create-next-app@latest quickpoll`. TypeScript, Tailwind, App Router β€” alles yes." - -*Tim wacht tot iedereen `npm run dev` draait.* - -> "Zie je de Next.js standaard pagina? Door." - -**⚑ Snelheid:** Studenten die dit al hebben kunnen alvast door naar stap 1. - ---- - -### Slide 3: Stap 1 β€” Types & Data (0:07-0:15) - -> "Twee bestanden: `src/types/index.ts` met de Poll interface, en `src/lib/data.ts` met onze in-memory database." - -*Tim typt de interface.* - -> "Een poll: id, question, options, votes. De votes array loopt parallel met options β€” index 0 van votes hoort bij index 0 van options." - -*Tim typt data.ts.* - -> "Drie helper functies: getPolls, getPollById, votePoll. Dit is onze 'database' β€” later vervangen we dit door Supabase." - -*Checkpoint: "Beide bestanden staan er? Door."* - ---- - -### Slide 4: Stap 2 β€” Layout (0:15-0:22) - -> "De layout wrapt elke pagina. Navbar, main content, footer. Verandert nooit." - -*Tim vervangt layout.tsx.* - -> "`Link` is Next.js client-side navigatie β€” geen page reload. `metadata` is je SEO titel." - -**⚑ Tip:** Niet te lang stilstaan, dit kennen ze. - ---- - -### Slide 5: Stap 2 vervolg β€” Homepage (0:22-0:30) - -> "De homepage: een Server Component die alle polls ophaalt en cards rendert." - -*Tim vervangt page.tsx.* - -> "Geen `'use client'`, geen useEffect, geen loading state. Server Component haalt data direct op. Dat is het voordeel." - -> "De `.map()` rendert een card per poll. `.reduce()` telt totaal stemmen." - -*Checkpoint: "3 cards op localhost:3000? Mooi."* - ---- - -### Slide 6: Stap 3 β€” GET API Route (0:30-0:40) - -> "Onze eerste API route. De folder-structuur IS je URL: `api/polls/[id]/route.ts` wordt `/api/polls/1`." - -*Tim maakt het bestand.* - -> "Dynamic route: `[id]` met vierkante haakjes. De waarde uit de URL wordt de `id` parameter." - -> "`await params` β€” Next.js 15 ding. Params zijn een Promise. Vergeet de await niet." - -*Tim test in browser: localhost:3000/api/polls/1.* - -> "JSON! Test ook `/api/polls/999` β€” 404. Mooi, recap klaar. Nu het nieuwe werk." - ---- - -## STAP 4-5: NIEUW MATERIAAL DEEL 1 (0:40-1:15) - -### Slide 7: Stap 4 β€” POST Vote Route (0:40-1:00) - -**πŸ’‘ Theorie-moment: POST vs GET** - -> "GET leest data, POST wijzigt data. Het patroon voor elke POST route: params, body, validatie, actie, response. Vijf stappen, altijd." - -*Tim maakt src/app/api/polls/[id]/vote/route.ts.* - -> "Nieuwe folder: `vote/` in de `[id]/` folder. Zo krijg je `/api/polls/1/vote`." - -*Tim typt de code langzaam.* - -> "`request.json()` leest de body β€” wat de client stuurt. Hier: welke optie." - -> "Twee error checks: 400 als data ongeldig, 404 als poll niet bestaat. Altijd beide." - -*Tim opent browser console, test met fetch.* - -> "Plak dit in je console. Zie je? Votes veranderd in de JSON. Onze stem-API werkt." - -*Checkpoint: "Heeft iedereen JSON in de console? Mooi."* - ---- - -### Slide 8: Stap 5 β€” Poll Detail Pagina (1:00-1:15) - -**πŸ’‘ Theorie-moment: generateMetadata & notFound** - -> "`generateMetadata` β€” speciale Next.js functie voor dynamische SEO. Elke poll krijgt z'n eigen browser tab titel." - -> "`notFound()` β€” roep je aan als data niet bestaat. Next.js toont automatisch de 404 pagina." - -*Tim maakt src/app/poll/[id]/page.tsx.* - -> "Dit is de combinatie: Server Component haalt data, rendert Client Component (`VoteForm`). Server = data, client = interactie." - -> "We importeren VoteForm maar die bestaat nog niet β€” dat bouwen we na de pauze." - -*Checkpoint: "/poll/1 geeft een error β€” dat klopt, VoteForm bestaat nog niet."* - ---- - -## PAUZE (1:15-1:30) - -### Slide 9: Pauze - -> "Pauze! 15 minuten. Stap 0-5 staan er. Na de pauze bouwen we het interactieve hart: de VoteForm." - -*Tim loopt rond en helpt studenten die achterlopen.* - ---- - -## STAP 6: VOTEFORM (1:30-2:10) - -*Dit is het langste en lastigste stuk. Neem de tijd.* - -### Slide 10: Stap 6 deel 1 β€” Logica (1:30-1:50) - -**πŸ’‘ Theorie-moment: Client Components & useState** - -> "`'use client'` bovenaan β€” dit component draait in de browser. Alles met useState, onClick, of interactiviteit MOET een Client Component zijn." - -*Tim maakt src/components/VoteForm.tsx.* - -> "Vier states." - -*Tim typt elke state en legt uit.* - -> "`selectedOption: number | null` β€” null want er kan nog niks geselecteerd zijn." -> "`hasVoted: boolean` β€” toggle na stemmen." -> "`isSubmitting: boolean` β€” voorkom dubbel klikken." -> "`currentPoll: Poll` β€” de actuele data, updated na stemmen." - -*Checkpoint: "Heeft iedereen de vier useState regels?"* - -*Tim typt handleVote.* - -> "De handleVote functie: dezelfde fetch als in de console bij stap 4. POST naar de API, update state als het lukt." - -*Checkpoint: "Tot hier mee?"* - ---- - -### Slide 11: Stap 6 deel 2 β€” UI (1:50-2:10) - -*Tim typt de JSX rustig, stopt na elk blok.* - -> "De UI heeft twee toestanden. VΓ³Γ³r stemmen: selecteer een optie. Na stemmen: percentage bars." - -*Tim typt de map over options.* - -> "Elke optie is een button. onClick selecteert de optie β€” maar alleen als je nog niet gestemd hebt." - -> "De conditional classes: `isSelected ? 'border-purple-500' : 'border-gray-200'`. Ternary operators in className β€” zo style je conditioneel in Tailwind." - -*Tim typt de percentage bar.* - -> "De percentage bar: een div met `absolute inset-0`, breedte is percentage, `transition-all duration-500` animeert. Pure CSS." - -*Tim typt stem-knop en bedankt-bericht.* - -> "Stem-knop verdwijnt na stemmen. Bedankt-bericht verschijnt. Allebei conditional rendering met `hasVoted`." - -*Tim test: localhost:3000/poll/1 β†’ klik optie β†’ stem.* - -> "Werkt het? Klik een optie, klik Stem, zie de animatie!" - -*Tim loopt rond en helpt. **Neem hier 10-15 min extra als nodig.** Dit is het lastigste stuk.* - -**Waar lopen studenten vast?** -- `"use client"` vergeten β†’ crash -- `selectedOption` niet nullable (`number` i.p.v. `number | null`) -- Fetch URL zonder backticks (template literal) -- Tailwind class typos -- Vergeten om het bestand op te slaan - -**Snelle fix:** "Check je terminal β€” TypeScript error? Heb je `'use client'` bovenaan?" - ---- - -## STAP 7-8: FINISHING TOUCHES (2:10-2:45) - -### Slide 12: Stap 7 β€” Loading, Error & Not-Found (2:10-2:30) - -**πŸ’‘ Theorie-moment: Speciale bestanden** - -> "In gewoon React bouw je dit zelf. In Next.js: maak een bestand aan en het werkt." - -*Tim maakt loading.tsx.* - -> "Skeleton loading. `animate-pulse` β€” Tailwind class. Vijf regels, professioneel resultaat." - -*Tim maakt error.tsx.* - -> "Error boundary. **`'use client'` is verplicht!** Error boundaries draaien altijd in de browser. `reset` herlaadt het gefaalde component." - -*Tim maakt not-found.tsx.* - -> "404 pagina. Test: `/poll/999`. Daar is 'm. Werkt door `notFound()` in de poll pagina." - -*Checkpoint: "Alle drie aangemaakt en werkend?"* - ---- - -### Slide 13: Stap 8 β€” Middleware (2:30-2:45) - -**πŸ’‘ Theorie-moment: Middleware** - -> "Middleware is een portier. Elke request passeert hier eerst." - -*Tim maakt src/middleware.ts.* - -> "Let op de locatie: `src/middleware.ts`. Niet in `app/`, niet in een subfolder. Dit is het enige bestand waar de locatie uitmaakt." - -> "De `matcher` bepaalt welke routes het raakt. API routes en poll pagina's β€” de homepage skipt het." - -> "Open je terminal. Klik op een poll. Zie je `[GET] /poll/1`? Dat is middleware in actie." - -> "Nu loggen we alleen. Bij Supabase wordt dit authenticatie: is de user ingelogd? Zo niet, redirect." - -*Checkpoint: "Logs in je terminal? App compleet!"* - ---- - -## AFSLUITING (2:45-3:00) - -### Slide 14: Huiswerk & Afsluiting - -> "Wat hebben we vandaag gedaan? De hele QuickPoll app van scratch: types, layout, homepage, API routes, detail pagina, VoteForm met animaties, loading states, middleware." - -> "De Next.js flow: Route β†’ Page β†’ Client Component β†’ API Route β†’ Response. Dat is het hele framework in één zin." - -> "Huiswerk: app niet af? Afmaken. Wel af? Probeer de bonus: een '/create' pagina met een form en POST naar `/api/polls`." - -> "Volgende les: Tailwind CSS en shadcn/ui. Dan geven we QuickPoll een styling upgrade." - -> "In één les van nul naar werkende app. Goed gedaan!" - ---- - -## POST-SESSIE - -**Na afloop checken:** -- [ ] Hoeveel studenten hebben werkende VoteForm? -- [ ] Was het tempo goed? (stap 0-3 snel genoeg, stap 4-8 genoeg tijd?) -- [ ] Terugkerende problemen notieren voor v2 -- [ ] Was van-scratch beter dan doorgaan waar Les 5 stopte? diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Lesopdracht.pdf b/Les06-NextJS-QuickPoll-Part2 2/Les06-Lesopdracht.pdf deleted file mode 100644 index 46cad4e..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/Les06-Lesopdracht.pdf +++ /dev/null @@ -1,410 +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 22 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 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -5 0 obj -<< -/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 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 -<< -/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -8 0 obj -<< -/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -9 0 obj -<< -/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -10 0 obj -<< -/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -11 0 obj -<< -/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -12 0 obj -<< -/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -13 0 obj -<< -/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -14 0 obj -<< -/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -15 0 obj -<< -/Contents 37 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -16 0 obj -<< -/Contents 38 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -17 0 obj -<< -/Contents 39 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -18 0 obj -<< -/Contents 40 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -19 0 obj -<< -/Contents 41 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -20 0 obj -<< -/Contents 42 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -21 0 obj -<< -/Contents 43 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -22 0 obj -<< -/BaseFont /ZapfDingbats /Name /F4 /Subtype /Type1 /Type /Font ->> -endobj -23 0 obj -<< -/Contents 44 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 26 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -24 0 obj -<< -/PageMode /UseNone /Pages 26 0 R /Type /Catalog ->> -endobj -25 0 obj -<< -/Author (\(anonymous\)) /CreationDate (D:20260317132407+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260317132407+01'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (Les 6: QuickPoll \204 Van Scratch tot Werkend) /Trapped /False ->> -endobj -26 0 obj -<< -/Count 18 /Kids [ 4 0 R 5 0 R 7 0 R 8 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 19 0 R 20 0 R 21 0 R 23 0 R ] /Type /Pages ->> -endobj -27 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 442 ->> -stream -Gat%^gJ3Ad&;KY!MRc\rf:?>GG\PZ@d\r.pH;,frg&iqG5/1#IV^P7K$!H1npA61$K&d<_Ha3danSiW3J->il:^I;S#^8Jb"F*He-T":>!\9fI6YUgQ0`!;-S>cU[5U^9dC(Nd6B`o#FV"gI-)XsG&Et$-??SVfTTCrseHNlh]ZjQO65Cp.L\cm-aUT]_hrfSiKkMSS@?bi>(A+EJ-S<3k1oCUK&WX:FC&o)kGcq*I:>O\CD^ZjPb49EWFKI&a8<^bYAnDe`sY=TcOaUF'.J@@A-VlNDVd#B,e^ed!km8]6MIK,(RWXR7]$oA<3VD>YU@?rNDLtCrcfGendstream -endobj -28 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 656 ->> -stream -Gau1,d;IDe'Sc)R'^#g7#`OgS3t?HRCB8-cKgf8>`bJ5iB]:X=2iX`u,V9d0g8Yme/4GHegZSHsfEZuV_"Kf@!3S)%2aTb1fag?i,\/hfbQa+1ga8k+$<*K^Os#YrN\le<1MO3e'JL[CW=W/];G#&2o)YtMTaWMX:^I/S2^O,j$:K)6Y54\bJ(F+"9:.`LEN9Jg*[#gc<<#R#(P1SiB\a7bVo2`TIL0GLI!,"'Tg,KY[XN@#$hmVBE;rk??dhbcH[JoTlogp@9$bJO7!,tt&U:$ltAEJ/ls9VW=!%8_gF56fataBk0M"g?k&`C_2ujXLE0dFDOYk,XMiendstream -endobj -29 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 730 ->> -stream -Gatn$d;IYl'Sc(%MK@=q8i=';3!i[R[SE:B:>@GYkZ4u!^M>dC&NfP&SCohno-,A7L"s4[9%d=L3Jd=nci?mqDH>FNL@M5t7-4iV)";,5?*DR7[9-/dP30-JKJ0[5h\;YduiL6k("rAn3$P&]0\);YI6%@s\"Zh;iY+"V!_mM7rRVQsSkmZ!u28N@1q^=l-P(IZhi`j8FNFIGq3/lrL$F-9iTl_NdTt"$9%U'f*\R%J:BDpo5X9Q:j;T_8(4\R]6Wo?;`lss+!.Pn'\eADct!@>"8%Ij-+Rq**6C*IA"9-u'rk&3&Ea>HP5;cEkA&!Ym1?Lu6WZC5[BH+1#$,O*]aI3OmjhbF4cl]SBK(Ra];C9V**$G1cjB\\jV9\`Y3B_B_<"f!VpTX]g&<+6Y7BUABpTFc;j7?d>A3b`C":(!#.$?LHJ'0cW,-Ioj'>;g_AP6l&eCq%OgCC/J6t@8Y-KXXm@UK)iPhibTs39YAZ7Eb1etD]^^t9:Td]D[pd'B$8(.O(!WFuALHedA#eh&E/CV\[,nf#^bmWG'poD-'t%a4LF[$Z/XLP%liAZD+u[3#U;dQd@hmhZ3[m.L~>endstream -endobj -30 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1340 ->> -stream -Gatn'?$"^h'Sc)T.o*Y=5XU4u9-8&A[[B$PGES@>B%dX^`>TLC,:"J8LcrTS]k8XDAIjH2Zbq_R`(K3TcMO2S,Q^/Y8H!b<"F)_U_-6to_Jf^4HQ7e/i/cj=k7dEjP"2)dE=bJS&s%`1_;k#t4HSpTKh!eI;KM3_/HtF/Z["Y?BILZk1us7=(Xna]TOJDe$S>FF:18CV;C25H^Ip/*0YDtq:F>p.r9PZdU.*uP8d+ST8Do78a#NiO$LmshiOFH>0@FbkdU77U9=Y'$74G`EV3]e6AKYOglT?gcf3#OqR84SOV[.K]6-"@+$A+^arrSh](*NUmgTq]gf&2?+ebE]1):Xn'"OV-N>oj4Z@da&NLkC4DoXM^1rZX(6#T6#fTO.f$gJqQn82@<)R@G]#OJaOT[@dqVB+W2W'M;XVg&,K@^`m_5piD.`9(M>`K]gifpj?Y)QeU`!9:P]5EYa.qCPC0BC3cW_]BI2(o^7cq'=%$N&[?sh_!&B60"dW",$\^CN?'uXSrfV&B0e&9Nme!4EiD*bCRFs=t`oI%o;B&Jok'Zk#"Y!teQJgmqjAD;m9oMR>,D-P!2E0aP0"T^PX+/VEth.p+eqC<>74Ldk"_PV/!uUcW4RZ5DAt'2@*?>H]<\\Uk8ifi64Z9MKB07Y9:GI*g0-LBIlBm[A(S*h5)$_s@/#00ptUSO#"OpFO`cKKEbuUfSP8%>;9m%FN10~>endstream -endobj -31 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 236 ->> -stream -Gatmt:CDb>&B4,8.Gh[r0d?!X5B;VLGV$t3J@gt22pr&=06N*5,$=G`bdsl\1;ekg&8i#G2/1f&'tPkdZb/noeK!]YZfodANdpgXSd\u\U6#o?VY?jfgY>gk2Y&sjc57Wh0_HB24(H?h-6p9SE"HqeRs,#~>endstream -endobj -32 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1445 ->> -stream -Gaua?>E@c%'Z]+o;]J5+`Dk+$NcUtlW[dD7&(=C"b9IO5&ehR7gE`X(6@AO]%`:PE@#X/`KbPkm*WL/-ro[_nT(jFN9e1bOVVp;S5T$@+5f=g"]n[-IS#744+%]oh5Y7>.'HtMX4=''GIuB0In16<+!fZ5GpC7![kej&IW_AN;hhD"[%Nd\nP1b^5(U8MR>9R[kRa^diTjP8+Cj4K*:?kLr;R>)$:IRQK%e&"aI:X!JGm#oh&"f;F__;?G`hB8ZcY5>hJG"=YQ_)T[fVm(eBf2Q/.CsB)S1mktp1t$eL59<\3V7>,C$'W.]MWM?ng!"0`4)*U<6(lDJ(e0WB9"%cbBiFcKe^P@p?T)s,>7KT=S5\s0QY[)]a&S6crn3O4oqM]k1$:k@ldN"MH-t+Circ?UY?>!`BE_(,k9'o/7/7rfbPBS%nbM>FC8^)Yrd-8NN4Vp+(3EPs=KLde)G^OA+l:L63_nb(F/!bZPZc%ANNENHA(Q"dfu#$F.pNGM].n\B1i)AsUD=pL4FD#))+Y3-\YH;<9u^R"%/FK0Rbd8m/VqW];oe[5X'mp7=>iRe9XV@RP#nJNFEKdPlI%^O`FTYpR3n=\f`olfpAG)qfYj6AG2CW+*I(Bq[Crq9<+$me/5!?O(HZK:+ng?5`1e[T(\$$f_&;l>-U"3X4^jbhIAJ_Es@=o;9agk9GgDV5X(AAP<4Gc#"Hl6d2P/t/#>d:,SHmLoQfS[!Ccl]N8!WQN9[?s@1rH$i`?#NJ<2(00r5?5\Y#1o8)-;DO\"h6SNr=cP%/O50.NMoofmI4u(ulKo?!:gRaha,r4l)!\N.G6]"_-rQM>j8\^(L$m9'Q3**6"%Sc6[ee(m*dt5cZeM,5kT!r,6E:]IPb!(YR(,2M*Ct^YJ[Ie+T4;$3c8@i2r/r;peY0@3fcE4st[k/Pee\0PrugGot^OU?J*kgC6=+p9Q9!E5+L4rctD[2?-l2l;5<@c)T&kAs`>-R"t#$(R`khI\eIoV^0g!Wd#hjbC#nl>VYU+du*fR7;*AnZ1@k/UiG09epd@ebV6[,29j$8>F8^rr&Fu:qu\AKLEl~>endstream -endobj -33 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 224 ->> -stream -Gaua84U]+\&;KrWMK`kWdTlC'iZRIG,%U&;-l"f,i9#O`g8]s.kYR@n,Bq6m:IkbKK=\^6J5nq@+]+#;";tp8[TFns&bJ2"K!Z4tKF_7$YDD@d>>e%Y]98]T_*GG?E4DJB1LJ&DATGZuNR0+R/'+a'b3q@=)/7[eFrWkA(I+,dg`H3es1T2Q7An=d6[e9%b7T2=s6DU#iKMSX#YRCMnG@\Y*jkKjH2~>endstream -endobj -34 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 929 ->> -stream -Gatn%gN)"%&;KY!MCho*e$13%nu7CY1R!!FBp1CD?0R%E,&msu,E!PanEsT:i0#u0FmmYbHHtJ-F#fQ#U].ZAC"Q8,JE@lVHp9?ZH?)8'[t(6FPd.#h!.93=*uhQ8DRj3mAV2V`r(o,^=7G33dtPud-2X^qAGjIJuh.SghK(1DEMD+L\5$9:#eD)b),_`]LeZFdo*hr0DLJ`5NU7;KGHU-"WRA`c%#FB1HSAY.T)[d^lWc2s6]\W3@jm1j0BJOr&O`dLrmPdt2XBkN!N/'T&,RhK[<["[=O%k&TUrVq!$o[A/OX!udDblEA>(_=*g/%Et%*R7_@e;V-E(gIqW`3)2H7rip@@iP.J+WE3`[0VWq-PW8`2(1,PXhk#IlUM;2VTQ\V_9r)#1#0pQkpY'WK+rXH!$riS=T~>endstream -endobj -35 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1261 ->> -stream -GauHL?#SIU'Re<2\FkcN,*PQTqXWcWkcb(<\i*37]i9>T/D=-;M^h/n#n6k.>tL'V+S![Y>[\mQBNk0"n=F\S8V,fX[_LuEJ6*&e^J^^5Op+\ME(Jj$0?Mnf1O>G1,KH^%&47:4`sW3mn);:sNrg*ni%PpeT7R`_R,_]+ndM-^q_7g'%>J9+8/$T[-G\\GB,0nb>e[gFi(pfj)`kE((AjCJGO%t$MFK)7nHPYiMm*?("niWobgNIiN#GR"<%8\0&Q+F_YB^Hm[]6_PCu+?*,4Z.\'6Q3)$dC4U\R*nUq]c'`]gXF,FsW0O:I>WO^kmYn*efP`p(L-a`\e_oh1MY[9NoNVVNOQh%c3()I3n/NQF26+//472.WNh;Kt8"6oXt:rOP'liY]kq7p7QkTf>W`=B0%ic'LX>$oXR-3V&!g.bn%t-bZ)$M@:t+RP6$JP,'Ltjl/",:am$f*O9-.7#6C,BO\[\+q\_Z$HA[!P27)%cSpu)O#ncO4[+RWDDaQiYC%Z>0aD[:(c6TQrPb3`bq&g+TWel0*DrjXWfn(C?n*H\$1rH.6S"sB9r?V/S9pQT+J3&leXFGN(9R:GT\m14l9iPrU9UU\BRYBT\jpnb!KcDMHbr7;OT&doBa$A:q>@L\K8/F\7TGGm%pDlqH/->gp+q,/f`3Z7oD'LM1sio$"q!@paci6Zc$QBiJT@"f]*@-"?RAr_?P_B=Ii\)K529XjXl9kHlO=,4Q1&n>6mB!V3CH-hM&D)0&m"LhbO./MZfYC30*[#;'?\pu1CpqV>AR]724EpF//]ULPsc0Bp"JXrnS>qRU/G#'BK0T^!99*mQF=n'V#.&'=:AjmoAu^tCqo$N2=~>endstream -endobj -36 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1051 ->> -stream -Gau1-Df=)Y&B--Tj!c5[n\QOopcj/nYr&=L"*("#B/)d*XCG?@&Un"e=P\0kMMpDpb`=O:B6[,YS9^:>`1Q9U>VJm:nVQO?E[=jNO.*%"_SZu8J%\PoC.8QfaY/].(`-N#lt-"q`q_NpNb6_%49U).#Ukb"sZ7dXcJ+Se(@=RR/@O1o=5fJRrK\q[Q]AO#7YuU=Z(?&iHCP^fYF5`.i9_?f"U-FElk_je`a7AGs^>>/VQ*gI"(\P-egGlPaB)rJe_=N]#OSCCN3[ieW)#\nV%TJGQ-D[.l>7NUZn7;@+odhZG3#T8APo6d=(g2!8I)2nGXCB1+Os1"j_n-)fa9V9o&'EDgBm,*?hrZ7*duEZG#%F/8!8n>DOmiErC(]c\96bZUE`fDBDCXnBR:YF=f[83>;!nr)S(Z,p>9K_mZ[G?a5.J4K.2hgoDCr)%GA`f"W\^4kNRaO"^ZJE)ag.k8-ursM>8#CKOCQT?D`m:OM"?'@\+VoH*hQnVo,@_29@C&3:=B$V?ge%"m.[.j>V"W-"aWbr/qP>tA&&^@Gm$QQpGB+W0e>CbNOuhnb6X)##($$\0]!oBU@RoGpN$/'iI:W/dhg.,T+Y91>35q@Qol2POkE:O+6a9-G~>endstream -endobj -37 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1454 ->> -stream -GauI7D/\/e&BE\k;]La)OLnI)WCMEYc\8`XN,]Ml*T5h>Qf&(BQM/8G>%l!.J1'F[gmX==f$5V@/<+nC-C\o+B[BF@4M%1OfTp5YNQ0$/UGEj,IUE/\J[q9'*b;)$Ocu?SgNbVh#("V1IqE0P>Liie*[=0WmfKK0P.K.`'E1VL[-qW2l)H![?D0pq:))28#u-Z=F<#O0EjTZ$s[DF(h^868Joo*j1sM[tE$r!5&_gI./br/#bVrmG<02DnM.k:V^'==XdGd-R*&9#$eAV)&N`.+NZG;pmPhEK3Vm*Es5C\0a$/?-0Xq>lsSK8q&*EMDOi=u?Zn2C&o1n;f%/HMi4$Cdh_:>uRDV&QB`nYt\o[dX48bLbL;$IWg4B5E2Q0(JZ"Y9m#Jr);Fr-94bLsnkPA_2g!ikY8=m$[4#=Rc5q$>0@.U^[YP_Kg2([B!PG;Vabb::gCSf]Mob12='"2T%qIE41%Q>scr>eb*TR_uf?-e5,g)LXL34Jq*\U/+(9$BMO2QfI&L>\uUi[ZaP1Q>rB%=LKMV\>gOC',gO@GJ]g;7P(".`68V0dWpSF`u(cK)ZrQ-Q!D,nSE2:6/A7u.kV7$'Y'--1JriL!pCY$9'P\m#d9AfJ^OWg'cWIbsLhq)_m.d#0eBkPWDc,F!>Y,a$bEVY9;)<[Nh);nBJ#%[Y3]e2&`ZLXkKidA-&h\kF,\d_6bVST\blU#7)l'L_LB#lWtfH:hsd=_Uuf;&17Df;DL7%?o(EAV?>YITs7B#E%T@?I8!DX0rKaP^oT$u7CD-Q?CoY/!!#4pLmf_,bQRX08pL*rA,:/$l5/16%6PX2#XQ*s#a-/[eimScA?ci*t8!4q:EdeE,Tp@(QGbp)a(&*H8Hn69dOKDrWUt=Yki#hj"ML\kD93o\9!lWtROk7g"6*C[boQ^TA/4ifF\_XE^>SUMn.rbO`5(XR#'&[#u*_;1t8FkJr*Kmc=(a=EOD14fGY<<%puS$AIZ)]H^L=(3),^uo`@Y>$1c#WSS(MF'@$BpX)\9&.Wh7GSR,C^h&LmX](j8*U;;N*Q?RA&Wk.O]02m$ULF4D,0q,+=])!,Bd"3[r-VpRZ6E0^"3MAj5Lq:aAXX7ZQ8Ssh@1Yi5:X]$2ed`^Ua&OU+Ki1uAX2&DEPT1!jXT?%k3Wj2?qoJ;qns$;,YT"^)[O>+W'-dTObAbDaaBPb7UXbNUc!.8^k0)~>endstream -endobj -38 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1416 ->> -stream -Gau`TqeP4]&H4hB`Pc#6;^t5JH<>@V(327YEjXGV>7,KrZF.,`dAs3\Yga5>R==4(@#h`dnIgVpqR&so7>pW0Sb&>THUk!rhTsBn4)nKVSknM'6oumH'X=e_%`6RT>//Jr-J25.82qBjP(U#+8C*gQ*ZD6H(4\]SJlGVPr[*Iq_e$F#GFbXeojRQ6):39;7_E3=&G8!+B"Sl9&ZTN.bc^agN@71:G:Y\"ULT;k2NQko;mN3+FLip]`Zp^*k]I/KAXJQ)VD?L4md%CZFh3a:HZ7;jMo(.^nC]pA$-nC2_^;FUA-<2`4/n0_-@iOL>?YFrpaPrhN3qPWog.B8>Go^*1L6pbTt9-rFs$[r=1$1m+q:J`Z7CE1b]DeZ?XV+PCd&\3+rme"f>,j0c$DhDPL%17WA$tg:$J9Dm!KMcWT!K+2Q5D(o(["U!lc=G/WP(/aC0r\LG(I=PUa5Z;As.hO"Ul.n$C%LYBtG#)^lK\r1ZmV92=s(mW2aY("n2V]CakrOF(p7=gIDdPRmt&/AreeBT2`Hpcki.aiC(0C%/7cr1V8Vf*Tj=B$8"XSQ+Dk%Fmp(N3HEE39P,TR[7o1YILhl`"CFG9[aeP%C3;O_?<*"8~>endstream -endobj -39 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 226 ->> -stream -Gatmt57>=^&B4BkMDqsRBs,^qU,WF%EY1(<.8#;G3k5"A]sK75'fq##*$P'gpKe,_Mu%MP!NUaF+]+#7"DOhI2HU3((%dsE?5fh\%Hf6E3CV0^1P>-&L)KGF.`+VQ_8gDU8i"U^ZH@G%)6_oVG!`XY&^JITbKuT($aZeEjVh(,op>OTh.+GgQ3N,t/q6P+1a[M'6[PFH=B!)Po*L#F9.'H2^CX3'8:i#~>endstream -endobj -40 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1340 ->> -stream -Gaua?gMZ%0&;KZF'XPM%llqT]ME3%XJ[S+"!^Y2PHEX?CO=f267]3)]`.m5pmmp&u^tK7*@9OL%+Go1ZEB$>b&ciAiV#?os!5D9:Hp`E\R*:+8PJQ;qXs[jhccC0#k*lheg]0HtR'pW>m"^Y,,MA*2]ec=gkrZVQ5cnWTPPHj(*&&i7Qp\XtBY[G8/7A6E7O<9qE*G_MOtY,>>)5M\%rbT@j//dHGQa3`0_qt^q1udq4NuL=0aT-9hDL.*Gs4Sq]_#K/4%*WhC--"-RrSTo]]2Y!I]6_Dn5r"WnlZU%*2^B3;',n46L@O0==c(fk.a)hcL!u::&jgC>T,r7"AW]@OKF7s$2diLo'B^XMlgGajL)i!_(O$hr2XFH`G.oL8+X-">W(nY_l[1#mUjZ!rE#=UCXha:+g5lH([E'L2.`a[=T'VTE]-4WjVnld9;_'ecP=M":;,uF@)8@)#dF*7pkV@j.Bgf?[TNi?oHFYlbT^2-S![jc3bSfM6L36phF]rc9T4l6(_Rk,K;Q9o@J!X>`i2`o$Z8l/\*-,sD("bWsq>ZsC#7:!-$gBXb[QQ,\KqjWZ,l<(4f$WobHJ'lO>ZD4p[egbQ5XN)NA04CX<*c57k'(p4)[d.^ebBpfjgL7ZRn&[bj`-8DNEaIhTQkjBEm4H.a"78ho;Pl+fY6?KH%0R:r/'*n=K7Z59Qfp*CgHAE+4e]IKbAW]G2-m^?OPu?CR\4eI&nJYTU'9Eo3hoSu)DofI(=QGFYJFNbWY1E!+Tl?T$?XMsa"VSLa=c5&sD'7T;52@TL[rpAg-uG^5UI(rX0CSoP`KYEE7@>O9ISp`$oOiT]@-[>rg,$L1YGB\"(4RW;8VoWHTi),/@u5]hkn)0hH*5=jk(PYSNJZFN94hUP7(=[9$I4dDNen"-I@AjB20`mBPH8n>[n6Q!F&?I%aK_\a.:0&78`Gh5=D^hQlR=3,=31*AX6M&k6>^R54\QNoJj%feg,*rAoaK^h\5;(SK.bVp1*85IlrsSB*nEV!#N_.te@2P0euQr[>OA][j-^lk]6k^HZt"`pIfRh8c78~>endstream -endobj -41 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 345 ->> -stream -Gatn`9hWAX(^KPW5K07d3h^D^2LRsU6."'lKcG<9?.RR!c)r!lr9gGQMTCGj66IF)cguLH&=@2hpI,2>$2>#6$6Zc"mW"$tmY1=GdFUs]H4a_.L*R5A0O^d:e@%jJ7;CAJQmF2:2bha<6YWNihn^eGb1nBi6okut!h?0kY0g9j6n(o;]92I^iYCHfZCb./jN#q,rrR5drBpNj+e5$iF8Z#"hJ27OA6N>?m2il*DO7eMUT!e%iA#93dN7NIkk<&jYGE0oL1YNf?L"]Gg\US`1A4p=%`]3!Se'IrNRYar`;!D.YGH_q$B5k+WD;/rS7T]HJ%`AC!7T"+q!j,loa0H1l$<~>endstream -endobj -42 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1049 ->> -stream -Gatm;CN%ZU'`GaQEN/_pU!8RfGL;'H$jd4ONrgCOdR&97L*^j&\D(EVJ*5tAG!dddO;03B'$'/TT4@beK1d#!mo]tdHjmno*4UOh%(OW2(r(XNn\N0Fmp#WC$[<:2&4N+7OiuuRqTSjs'N"j(Lr^)dYXBa#Fee((e0j.j:rakJ#S:kU+[cFjI+%6liXriJVpb^N;MBl[rX%U4%JC%PND-r!%h`gflWf']XcMoH3+e)oXuqV8kP@L>_E;iDQDuONArg.bE&Gd5+hf5O19Z%>VJ=/aD%KD2+3*/:T.q]!'E[_]ZRj%d@&7tUC%FU4L@A]&OM#)m(dW"ltJJ;0cn#YI^;"RfZhNZkS+8`<>Y;cErn_Sg)ia+M&t_Ka[_P-ii=gEMK!+!hHW2QUecUqBr)o'Ze>iO/Z@ohngJ_A[#YQ8#9$jfh3CmL5[*0TTJn`:U5ZDV;F[q/::^pqX*<_A[d6DV3Fq6>L[dpeA1]X+Dp%]r9?lf4RQ_'-uSp)%Rp<)Hp/3cX543h]ep3+),6N2:?A^p\79eom\NR.S\=7@_lV&GfXb16b]CnRj:p3j+9r.m%T@cfog'#_"pD!9FHVs"9Tl-aUI?r2nSf4^(IV:A:AZE'?WY'Ut%+AcO*&WZ.PJIh>L.bY`S*05Ee&;"J-"UqV%8X3ilG!4/nT`g(PYb_*mY^).nohaE)Wpes=6(3uB*R_XKZ-7Dm:GH,dC+TT40^qiCl3AlSN/#ZMVsJaCIj?bG6NAZV@"5OJVr76dI%`L_Ys3)YS&T]//.t+OdJ%LGIFM/Q&FII!*USnk.]TYRMFP,@)so5I'LKR]KImS'l7s>WI]endstream -endobj -43 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1256 ->> -stream -Gatm;gN)%,&:O:SD"CQ$P6gHp$e4P<=Ji!`_n_[tF9!hck%Q!827Ze)Zj2ba-$H(;T8g]Uk[!TC@8>OKahE;$qHi?4?%s*DM$!R6J'b/3)`q/69$X,>Bh<6/iT`$_X%VA5<'4(dd'(>6$Nfk$Qd`!`Zm-3!Wkc>PNWLWmGde/1M%95U^["/\7+lT>A0Bgq`%4]#Mlmh(pHOpKLoeKcQUX10`K6pqr>M,*)NiP=c;cUZ'fpJp#2=l,3eboG'nE?b%&+glGQ*DUFMMLb<'LjKD`!4'h;Gn(*&D`Bi2*(jRn;2YU>e-@RW70MT00Z!\YV&4j[le;m$sY?$J&!QbgCcCXP&bN.si,SZjBY84B(>^rh^#R8$^P[q2bEeWCa(KiP7>1,sIMU2$B`;j*J8QL9W4KutGo@XA--EC]dnjZDIQaMOD_X4(L>u;Fu\ASC2UanK`qk94V%KYTUG?A(JcGm(4B:Gs&*MYCgCaCY?'MHO5X]=Hu>CtpX"6bj'U*T):'9M#Cf1.&aeao=)s*8HuU"r0>>"[hVU=iu+m61_I[+Yc6SOn19@Fd1:U-b1Y\$OG*BqZ/,[S-X_%PH;Rsicro3d#!%EgR]Y>Ts86L^ua#]('i2tFsQ?hd)GO;3jrET6(FePu&;fjR/-mc$.f0t_F9a2kuPii;!"1:bD3s7Z6N=V]5/FuXHW(FmHD/f=BPJ-uZBeTD4rZ(U_m>:eOW>g(%q(Os0@`WiC:X>df,51n\.6gD8JB.Ap1K@HXN8,dlKq@2:Du!8W"Ur?q]'#JVHfA(QIi#IU$Y^.M>Z4k!qOs*2k_I5AnB-0M/g)t#m.8i1l96d47#aiK9f_mubs#!RF-k*"5m1_k7qg+'dVrFWCrFRA7QUiLBaL`\(>$^]4`;fhAHL%++"oXk-sJWha$BqKG~>endstream -endobj -44 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1552 ->> -stream -Gb!#\gN)%,&:Ml+D"Z.'2i?PLb&ifd:7APmlc*Z\;YL29g$#9DgW)BIEYF-+(Rfn0/+`!Fu+lV?>QNJ67iZ==ai5A$$#4KsKS;7-Ju#Ot8lR*oKPX!(,A;6PIUUJMLH&m(C@NC6U`melq<%N6p]#&-er9:uFl8/38!-:9n7He!DN(Xt-EjBMh*&$Bu7G):s1MR2[HmTh2Apo&lonYItK0-+8??@3>WHs#bT"KpBWFR2`8D1Db_C^^rVlZ#AMU7Dk+e9J=Yg#=VZ(9M_#ktP-PTOHhMK7VM1IY&oIg)8^72?k%?A:Y_M3YM(pHV,k+]Ndtc5lhW%?QId_MEUN=$GUBS5rH!-YkbrQ(!-=BmqpJ4QB*;,^'.F>L'kD.f=mj-_>Mt1,oJr5s8K*M;f;_!*2nh7+\[Y9c+>Zl8lD^-HVQ"9\k&DO-FtuGVN.N;B3/`&m<[QbV4!;BsX7WZ3f/`c4bOr1OG7?PR@6)%QW-b@go\W)o>@]IT0i"$>r./:+QK66HLl\D*;YeS9A/#OqPFa\5Yh%)D=#RL,c(gRsZiA`nY-D)i;Is->2[8N=q"QEQ*L!/5Tm)W_KjQ2m`KckB.LA8Z5[o&Z"SG&Dqp(U*,REHf-f/1Y=!6QONnZ,D%db$F`(kd/SmRj9llmQ41WT6Wd&P>&L7264nN.>l>lj2NTgkgjM0V@Fedd_BF_spm\]ITpqELNB,0W6\u..L1RaqbfPFV+qO.7XK6$9DtGR%WQHS\NJ\?#c!UVhb7\Be6R?7A4VRXa3?J?$TjFWpTl\G#6/D/;$!"g;e\@40oTendstream -endobj -xref -0 45 -0000000000 65535 f -0000000061 00000 n -0000000123 00000 n -0000000230 00000 n -0000000342 00000 n -0000000547 00000 n -0000000752 00000 n -0000000857 00000 n -0000001062 00000 n -0000001267 00000 n -0000001472 00000 n -0000001678 00000 n -0000001884 00000 n -0000002090 00000 n -0000002296 00000 n -0000002502 00000 n -0000002708 00000 n -0000002914 00000 n -0000003120 00000 n -0000003326 00000 n -0000003532 00000 n -0000003738 00000 n -0000003944 00000 n -0000004028 00000 n -0000004234 00000 n -0000004304 00000 n -0000004617 00000 n -0000004796 00000 n -0000005329 00000 n -0000006076 00000 n -0000006897 00000 n -0000008329 00000 n -0000008656 00000 n -0000010193 00000 n -0000010508 00000 n -0000011528 00000 n -0000012881 00000 n -0000014024 00000 n -0000015570 00000 n -0000017078 00000 n -0000017395 00000 n -0000018827 00000 n -0000019263 00000 n -0000020404 00000 n -0000021752 00000 n -trailer -<< -/ID -[<4ba4cdaa716430c5e4593eab67d6901b><4ba4cdaa716430c5e4593eab67d6901b>] -% ReportLab generated PDF document -- digest (opensource) - -/Info 25 0 R -/Root 24 0 R -/Size 45 ->> -startxref -23396 -%%EOF diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Live-Coding-Guide.md b/Les06-NextJS-QuickPoll-Part2 2/Les06-Live-Coding-Guide.md deleted file mode 100644 index dd88506..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/Les06-Live-Coding-Guide.md +++ /dev/null @@ -1,1183 +0,0 @@ -# Les 6: QuickPoll Live Coding Guide - -> Dit is je stap-voor-stap spiekbriefje. Lees dit op je eigen scherm terwijl je op de beamer codeert. -> Alles staat in de volgorde waarin je het typt. Niets wordt overgeslagen. - ---- - -## STAP 0 β€” Project aanmaken - -**Terminal:** -```bash -npx create-next-app@latest quickpoll -``` - -**Opties selecteren:** -- TypeScript β†’ Yes -- ESLint β†’ Yes -- Tailwind CSS β†’ Yes -- `src/` directory β†’ Yes -- App Router β†’ Yes -- Import alias β†’ @/* - -```bash -cd quickpoll -npm run dev -``` - -**Check:** localhost:3000 β†’ Next.js standaardpagina - -**Vertel:** "Dit is create-next-app. Geeft ons een compleet project met TypeScript, Tailwind en de App Router. Alles wat we nodig hebben." - ---- - -## STAP 1 β€” Types - -### 1a. Maak `src/types/index.ts` - -**Vertel:** "Eerst: wat IS een poll? We definiΓ«ren dat met een TypeScript interface." - -Typ: -```tsx -export interface Poll { -``` - -**Vertel:** "Een poll heeft een id…" - -```tsx - id: string; -``` - -"…een vraag…" - -```tsx - question: string; -``` - -"…een lijst opties…" - -```tsx - options: string[]; -``` - -"…en stemmen per optie. De index matcht: votes[0] hoort bij options[0]." - -```tsx - votes: number[]; -} -``` - ---- - -## STAP 1 vervolg β€” Data - -### 1b. Maak `src/lib/data.ts` - -**Vertel:** "Nu onze 'database'. Gewoon een array in het geheugen. Later vervangen we dit door Supabase." - -Typ de import: -```tsx -import type { Poll } from "@/types"; -``` - -**Vertel:** "We maken drie polls aan als testdata." - -```tsx -let polls: Poll[] = [ - { - id: "1", - question: "Wat is je favoriete programmeer taal?", - options: ["JavaScript", "Python", "TypeScript", "Rust"], - votes: [45, 32, 28, 15], - }, - { - id: "2", - question: "Hoe veel uur slaap krijg je per nacht?", - options: ["< 6 uur", "6-8 uur", "8+ uur"], - votes: [12, 68, 35], - }, - { - id: "3", - question: "Welke framework gebruik je het meest?", - options: ["React", "Vue", "Svelte", "Angular"], - votes: [89, 34, 12, 8], - }, -]; -``` - -**Vertel:** "Nu helper functies. Eerst: alle polls ophalen." - -```tsx -export function getPolls(): Poll[] { - return polls; -} -``` - -"EΓ©n poll ophalen op id:" - -```tsx -export function getPollById(id: string): Poll | undefined { - return polls.find((poll) => poll.id === id); -} -``` - -**Vertel:** "Let op het return type: `Poll | undefined`. Misschien bestaat de poll niet β€” dat moeten we afvangen." - -"En stemmen:" - -```tsx -export function votePoll(pollId: string, optionIndex: number): Poll | undefined { - const poll = polls.find((p) => p.id === pollId); -``` - -"Check of de poll bestaat en de index klopt:" - -```tsx - if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) { - return undefined; - } -``` - -"Stem ophogen en poll teruggeven:" - -```tsx - poll.votes[optionIndex]++; - return poll; -} -``` - -**Checkpoint:** "Heeft iedereen types/index.ts en lib/data.ts?" - ---- - -## STAP 2 β€” Layout - -### 2a. Vervang `src/app/layout.tsx` - -**Vertel:** "De layout wrapt elke pagina. Navbar en footer staan hier β€” die veranderen nooit." - -Wis de inhoud. Typ de imports: - -```tsx -import type { Metadata } from "next"; -import Link from "next/link"; -import "./globals.css"; -``` - -**Vertel:** "Link is de Next.js versie van een `` tag. Client-side navigatie, geen page reload." - -SEO metadata: - -```tsx -export const metadata: Metadata = { - title: "QuickPoll", - description: "Een snelle polling app met Next.js", -}; -``` - -**Vertel:** "metadata is voor SEO. Kijk straks in je browser tab." - -De functie: - -```tsx -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - -``` - -**Vertel:** "Nu de navbar. Simpel: logo en een home link." - -```tsx - -``` - -Main en footer: - -```tsx -
{children}
- -
-
- Β© 2026 QuickPoll β€” Built with Next.js 15 -
-
- - - ); -} -``` - -**Vertel:** "`{children}` β€” hier komt de pagina-inhoud. Elke pagina in de app wordt hier ingeladen." - -**Check:** localhost:3000 β†’ navbar met "QuickPoll" zichtbaar - ---- - -### 2b. Vervang `src/app/page.tsx` - -**Vertel:** "Nu de homepage. Dit is een Server Component β€” geen 'use client', data direct ophalen." - -Wis de inhoud. Typ: - -```tsx -import Link from "next/link"; -import { getPolls } from "@/lib/data"; -``` - -**Vertel:** "We importeren getPolls uit onze data module. Dat `@/` is een shortcut voor de src/ folder." - -```tsx -export default function Home() { - const polls = getPolls(); -``` - -**Vertel:** "Kijk: we roepen getPolls() gewoon aan. Geen useEffect, geen loading state. Dit is een Server Component β€” data ophalen is direct." - -De JSX: - -```tsx - return ( -
-
-

QuickPoll

-

- Kies een poll en stem af in een oogwenk -

-
-``` - -**Vertel:** "Nu de poll cards. We mappen over de polls array." - -```tsx -
- {polls.map((poll) => { - const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0); - - return ( - -
-

- {poll.question} -

-
- {poll.options.length} opties - {totalVotes} stemmen -
-
- Stemmen β†’ -
-
- - ); - })} -
-
- ); -} -``` - -**Vertel:** "`.reduce()` telt alle stemmen op. En elke card linkt naar `/poll/1`, `/poll/2` etc. Die pagina maken we straks." - -**Check:** localhost:3000 β†’ 3 poll cards zichtbaar - -**Checkpoint:** "Ziet iedereen 3 cards? Mooi." - ---- - -## STAP 3 β€” GET API Route - -### 3a. Maak `src/app/api/polls/[id]/route.ts` - -**Vertel:** "Nu onze eerste API route. In Next.js is de folder-structuur je URL. Deze folder wordt `/api/polls/1`." - -"Die `[id]` met vierkante haakjes β€” dat is een dynamic route. Het getal in de URL wordt de id parameter." - -Typ: - -```tsx -import { NextResponse } from "next/server"; -import { getPollById } from "@/lib/data"; -``` - -**Vertel:** "Een API route exporteert functies met HTTP method namen. GET voor data ophalen." - -```tsx -export async function GET( - request: Request, - { params }: { params: Promise<{ id: string }> } -) { -``` - -**Vertel:** "Let op: `Promise<{ id: string }>`. In Next.js 15 zijn params een Promise. Vergeet de await niet!" - -```tsx - const { id } = await params; - const poll = getPollById(id); -``` - -"Als de poll niet bestaat: 404." - -```tsx - if (!poll) { - return NextResponse.json({ error: "Poll not found" }, { status: 404 }); - } -``` - -"Anders: stuur de poll als JSON." - -```tsx - return NextResponse.json(poll); -} -``` - -**Test:** Open browser β†’ `localhost:3000/api/polls/1` β†’ JSON! - -**Vertel:** "Probeer ook `/api/polls/999` β€” daar krijg je de 404." - -**Checkpoint:** "Werkt de API? Recap klaar. Nu het nieuwe werk." - ---- - -## STAP 4 β€” POST Vote Route - -### 4a. Maak `src/app/api/polls/[id]/vote/route.ts` - -**Vertel:** "Tot nu toe hadden we GET β€” data ophalen. Nu POST β€” data wijzigen. In dit geval: stemmen." - -Typ de imports: - -```tsx -import { NextResponse } from "next/server"; -import { votePoll } from "@/lib/data"; -``` - -**Vertel:** "Eerst definiΓ«ren we types voor de parameters en de request body." - -```tsx -interface RouteParams { - params: Promise<{ id: string }>; -} - -interface VoteBody { - optionIndex: number; -} -``` - -**Vertel:** "Een POST route volgt altijd vijf stappen: params, body, validatie, actie, response." - -```tsx -export async function POST( - request: Request, - { params }: RouteParams -): Promise { -``` - -"Stap 1: params uitlezen β€” welke poll?" - -```tsx - const { id } = await params; -``` - -"Stap 2: body uitlezen β€” welke optie? `request.json()` leest wat de client meestuurt." - -```tsx - const body: VoteBody = await request.json(); -``` - -"Stap 3: validatie β€” is optionIndex een nummer?" - -```tsx - if (typeof body.optionIndex !== "number") { - return NextResponse.json( - { error: "optionIndex is verplicht" }, - { status: 400 } - ); - } -``` - -**Vertel:** "400 = bad request. Jij stuurde slechte data." - -"Stap 4: actie uitvoeren." - -```tsx - const updatedPoll = votePoll(id, body.optionIndex); - - if (!updatedPoll) { - return NextResponse.json( - { error: "Poll niet gevonden of ongeldige optie" }, - { status: 404 } - ); - } -``` - -**Vertel:** "404 = niet gevonden. Twee checks: poll bestaat niet, of optie-index ongeldig." - -"Stap 5: response sturen." - -```tsx - return NextResponse.json(updatedPoll); -} -``` - -### 4b. Testen in de browser console - -**Vertel:** "We testen! Open DevTools (F12), ga naar Console, en plak dit:" - -```javascript -fetch('/api/polls/1/vote', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ optionIndex: 0 }) -}).then(r => r.json()).then(console.log) -``` - -**Vertel:** "Zie je? De votes zijn veranderd in de JSON. De API werkt." - -**Checkpoint:** "Heeft iedereen een JSON resultaat in de console?" - ---- - -## STAP 5 β€” Poll Detail Pagina - -### 5a. Maak `src/app/poll/[id]/page.tsx` - -**Vertel:** "Nu de pagina waar je op een poll stemt. Weer een dynamic route: `/poll/1`, `/poll/2` etc." - -Imports: - -```tsx -import { notFound } from "next/navigation"; -import { getPollById } from "@/lib/data"; -import VoteForm from "@/components/VoteForm"; -import type { Metadata } from "next"; -``` - -**Vertel:** "We importeren VoteForm β€” dat component bestaat nog niet, dat bouwen we zo." - -Type: - -```tsx -interface PageProps { - params: Promise<{ id: string }>; -} -``` - -**Vertel:** "Eerste nieuw concept: `generateMetadata`. Hiermee krijgt elke poll z'n eigen titel in de browser tab." - -```tsx -export async function generateMetadata({ params }: PageProps): Promise { - const { id } = await params; - const poll = getPollById(id); - - if (!poll) { - return { title: "Poll niet gevonden" }; - } - - return { - title: `${poll.question} β€” QuickPoll`, - description: `Stem op: ${poll.options.join(", ")}`, - }; -} -``` - -**Vertel:** "Gratis SEO. Google ziet 'Wat is de beste editor?' in plaats van gewoon 'QuickPoll'." - -Nu de pagina zelf: - -```tsx -export default async function PollPage({ params }: PageProps) { - const { id } = await params; - const poll = getPollById(id); -``` - -**Vertel:** "Tweede concept: `notFound()`. Als de poll niet bestaat, roep je dit aan. Next.js toont dan automatisch een 404 pagina." - -```tsx - if (!poll) { - notFound(); - } -``` - -"En we renderen de poll titel en het VoteForm component:" - -```tsx - return ( -
-

- {poll.question} -

- -
- ); -} -``` - -**Vertel:** "Dit is een Server Component die een Client Component rendert. Server haalt data, client doet interactie. Dat is de kern van Next.js." - -**Check:** localhost:3000/poll/1 β†’ error (VoteForm bestaat nog niet, dat klopt!) - ---- - -## β˜• PAUZE β€” 15 minuten - -**Vertel:** "Pauze! Stap 0-5 staan er. Na de pauze bouwen we het interactieve hart: de VoteForm." - ---- - -## STAP 6 β€” VoteForm Component - -### 6a. Maak `src/components/VoteForm.tsx` - -**Vertel:** "Dit is het leukste stuk. Een Client Component met interactiviteit." - -"Eerste regel: `'use client'`. Daarmee zeg je tegen Next.js: dit draait in de browser. Alles met useState of onClick MOET dit hebben." - -```tsx -"use client"; -``` - -Imports: - -```tsx -import { useState } from "react"; -import type { Poll } from "@/types"; -``` - -Props interface: - -```tsx -interface VoteFormProps { - poll: Poll; -} -``` - -**Vertel:** "We ontvangen een poll als prop van de Server Component." - -Functie openen: - -```tsx -export default function VoteForm({ poll }: VoteFormProps) { -``` - -### 6b. State variabelen - -**Vertel:** "We hebben vier stukken state nodig. Ik leg ze één voor één uit." - -"Welke optie is geselecteerd? `null` want er kan nog niks geselecteerd zijn:" - -```tsx - const [selectedOption, setSelectedOption] = useState(null); -``` - -"Is er al gestemd?" - -```tsx - const [hasVoted, setHasVoted] = useState(false); -``` - -"Zijn we bezig met het versturen? Om dubbel klikken te voorkomen:" - -```tsx - const [isSubmitting, setIsSubmitting] = useState(false); -``` - -"De huidige poll data β€” die updaten we na het stemmen:" - -```tsx - const [currentPoll, setCurrentPoll] = useState(poll); -``` - -**Checkpoint:** "Heeft iedereen de vier useState regels?" - -### 6c. Helper functies - -**Vertel:** "Totaal stemmen berekenen:" - -```tsx - const totalVotes: number = currentPoll.votes.reduce( - (sum, v) => sum + v, - 0 - ); -``` - -"Percentage per optie:" - -```tsx - function getPercentage(votes: number): number { - if (totalVotes === 0) return 0; - return Math.round((votes / totalVotes) * 100); - } -``` - -### 6d. handleVote functie - -**Vertel:** "Nu de stem-functie. Dit is dezelfde fetch als we net in de console testten β€” maar nu in een component." - -```tsx - async function handleVote(): Promise { -``` - -"Guard clause: als er niks geselecteerd is of we al bezig zijn, stop." - -```tsx - if (selectedOption === null || isSubmitting) return; - setIsSubmitting(true); -``` - -"De fetch naar onze POST route:" - -```tsx - const response = await fetch(`/api/polls/${currentPoll.id}/vote`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ optionIndex: selectedOption }), - }); -``` - -"Als het lukt: update de poll data en zet hasVoted op true." - -```tsx - if (response.ok) { - const updatedPoll: Poll = await response.json(); - setCurrentPoll(updatedPoll); - setHasVoted(true); - } - - setIsSubmitting(false); - } -``` - -**Checkpoint:** "Tot hier mee? Dan gaan we de UI bouwen." - -### 6e. UI β€” De opties buttons - -**Vertel:** "De return. We mappen over de opties β€” elke optie wordt een button." - -```tsx - return ( -
- {currentPoll.options.map((option, index) => { - const percentage = getPercentage(currentPoll.votes[index]); - const isSelected = selectedOption === index; -``` - -**Vertel:** "Elke button. onClick selecteert de optie, maar alleen als je nog niet gestemd hebt." - -```tsx - return ( - - ); - })} -``` - -### 6f. UI β€” Stem button en bedankt bericht - -**Vertel:** "De stem-knop. Alleen zichtbaar als je nog niet gestemd hebt." - -```tsx - {!hasVoted && ( - - )} -``` - -"En het bedankt bericht na het stemmen:" - -```tsx - {hasVoted && ( -

- Bedankt voor je stem! Totaal: {totalVotes} stemmen -

- )} -
- ); -} -``` - -### 6g. Testen! - -**Test:** localhost:3000/poll/1 - -**Vertel:** "Klik op een optie β€” paarse border. Klik Stem. Zie je de animatie? De bars vullen zich." - -**Loop rond. Dit is het lastigste stuk. Neem 10 min extra als nodig.** - -**Veelvoorkomende fouten:** -- `"use client"` vergeten β†’ crash -- Fetch URL zonder backticks β†’ werkt niet -- Bestand niet opgeslagen β†’ niks veranderd - ---- - -## STAP 7 β€” Loading, Error & Not-Found - -**Vertel:** "In gewoon React bouw je dit zelf. In Next.js: maak een bestand aan en het werkt." - -### 7a. Maak `src/app/loading.tsx` - -**Vertel:** "Skeleton loading. Die grijze blokken die pulseren β€” `animate-pulse` is een Tailwind class." - -```tsx -export default function Loading() { - return ( -
-
-
-
-
- {[1, 2, 3].map((i) => ( -
-
-
-
- ))} -
- ); -} -``` - -### 7b. Maak `src/app/error.tsx` - -**Vertel:** "Error boundary. LET OP: `'use client'` is hier VERPLICHT. Dat is een Next.js vereiste." - -```tsx -"use client"; - -export default function Error({ - error, - reset, -}: { - error: Error; - reset: () => void; -}) { - return ( -
-

- Er ging iets mis! -

-

{error.message}

- -
- ); -} -``` - -**Vertel:** "`reset` herlaadt het component dat faalde. Zo hoeft de user niet de hele pagina te refreshen." - -### 7c. Maak `src/app/not-found.tsx` - -```tsx -import Link from "next/link"; - -export default function NotFound() { - return ( -
-

404

-

Deze pagina bestaat niet.

- - Terug naar home - -
- ); -} -``` - -**Test:** localhost:3000/poll/999 β†’ 404 pagina! - -**Vertel:** "Dat werkt door de `notFound()` aanroep in onze poll pagina." - -**Checkpoint:** "Alle drie werkend?" - ---- - -## STAP 8 β€” Middleware - -### 8a. Maak `src/middleware.ts` - -⚠️ **Let op locatie: `src/middleware.ts` β€” NIET in app/, NIET in een subfolder!** - -**Vertel:** "Middleware is als een portier. Elke request passeert hier eerst. Nu gebruiken we het voor logging β€” later bij Supabase voor authenticatie." - -```tsx -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export function middleware(request: NextRequest): NextResponse { - const start = Date.now(); - - console.log(`[${request.method}] ${request.nextUrl.pathname}`); - - const response = NextResponse.next(); - response.headers.set("x-request-time", String(Date.now() - start)); - - return response; -} -``` - -**Vertel:** "`NextResponse.next()` betekent: alles okΓ©, ga door naar de route." - -"De matcher bepaalt welke routes de middleware raakt:" - -```tsx -export const config = { - matcher: ["/api/:path*", "/poll/:path*"], -}; -``` - -**Vertel:** "Alleen API routes en poll pagina's. De homepage skipt de middleware." - -### 8b. Testen - -**Vertel:** "Open je terminal waar `npm run dev` draait. Klik op een poll in de browser." - -**In de terminal:** `[GET] /poll/1` verschijnt. - -**Vertel:** "En in DevTools: Network tab β†’ klik op een request β†’ Headers β†’ `x-request-time`. Dat is de verwerkingstijd die onze middleware toevoegt." - ---- - -## βœ… APP COMPLEET! - -**Vertel:** "De hele app werkt. Van nul naar werkend in één les." - ---- - -## BONUS β€” Nieuwe Poll Aanmaken (als je tijd overhebt) - -> ~20 minuten. Doe dit klassikaal als je voor 2:45 klaar bent. - -**Vertel:** "We gaan een pagina maken waar je zelf een nieuwe poll kunt aanmaken. Twee dingen nodig: een API route die polls aanmaakt, en een pagina met een formulier." - -### Bonus A. Voeg `createPoll` toe aan `src/lib/data.ts` - -**Vertel:** "Eerst een helper functie in onze data module. Open `data.ts` en voeg onderaan toe:" - -```tsx -export function createPoll(question: string, options: string[]): Poll { - const newPoll: Poll = { - id: String(nextId++), - question, - options, - votes: new Array(options.length).fill(0), - }; - polls.push(newPoll); - return newPoll; -} -``` - -**Vertel:** "`new Array(options.length).fill(0)` β€” maakt een array met nullen, even lang als het aantal opties. Elke optie begint met 0 stemmen." - -### Bonus B. Maak `src/app/api/polls/route.ts` - -**Vertel:** "Nu de API route. Let op: dit is `api/polls/route.ts`, niet in de `[id]` folder. Dit wordt `POST /api/polls`." - -```tsx -import { NextResponse } from "next/server"; -import { createPoll } from "@/lib/data"; -``` - -**Vertel:** "We definiΓ«ren een type voor de request body." - -```tsx -interface CreatePollBody { - question: string; - options: string[]; -} -``` - -"De POST handler:" - -```tsx -export async function POST(request: Request): Promise { -``` - -"Body uitlezen:" - -```tsx - const body: CreatePollBody = await request.json(); -``` - -"Validatie β€” vraag moet er zijn, en minimaal 2 opties:" - -```tsx - if (!body.question || !body.options || body.options.length < 2) { - return NextResponse.json( - { error: "Vraag en minimaal 2 opties zijn verplicht" }, - { status: 400 } - ); - } -``` - -"Poll aanmaken en teruggeven:" - -```tsx - const newPoll = createPoll(body.question, body.options); - return NextResponse.json(newPoll, { status: 201 }); -} -``` - -**Vertel:** "201 = created. Standaard HTTP status als je iets nieuws aanmaakt." - -### Bonus C. Maak `src/app/create/page.tsx` - -**Vertel:** "Nu het formulier. Dit is een Client Component β€” we hebben state nodig voor de inputs." - -```tsx -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -``` - -**Vertel:** "`useRouter` is voor navigatie vanuit een Client Component. Straks sturen we de user terug naar home." - -```tsx -export default function CreatePollPage() { - const router = useRouter(); - const [question, setQuestion] = useState(""); - const [options, setOptions] = useState(["", ""]); - const [isSubmitting, setIsSubmitting] = useState(false); -``` - -**Vertel:** "Drie stukken state. De vraag, de opties (begint met 2 lege strings), en een submit flag." - -"Een functie om een optie toe te voegen:" - -```tsx - function addOption() { - setOptions([...options, ""]); - } -``` - -"En een functie om een optie te updaten op een specifieke index:" - -```tsx - function updateOption(index: number, value: string) { - const newOptions = [...options]; - newOptions[index] = value; - setOptions(newOptions); - } -``` - -**Vertel:** "Nu de submit functie. Filtert lege opties weg, POST naar de API, redirect naar home." - -```tsx - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setIsSubmitting(true); - - const filledOptions = options.filter((opt) => opt.trim() !== ""); - - if (!question.trim() || filledOptions.length < 2) { - alert("Vul een vraag in en minimaal 2 opties."); - setIsSubmitting(false); - return; - } - - const response = await fetch("/api/polls", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ question, options: filledOptions }), - }); - - if (response.ok) { - router.push("/"); - } - - setIsSubmitting(false); - } -``` - -**Vertel:** "`e.preventDefault()` voorkomt dat de pagina herlaadt. `router.push('/')` stuurt je terug naar de homepage." - -"Nu de JSX. Een form met inputs:" - -```tsx - return ( -
-

- Nieuwe Poll Aanmaken -

- -
-
- - setQuestion(e.target.value)} - placeholder="Stel je vraag..." - className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" - /> -
-``` - -"De opties β€” we mappen over de options array:" - -```tsx -
- -
- {options.map((option, index) => ( - updateOption(index, e.target.value)} - placeholder={`Optie ${index + 1}`} - className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" - /> - ))} -
-``` - -"Knop om meer opties toe te voegen:" - -```tsx - -
-``` - -"En de submit knop:" - -```tsx - -
-
- ); -} -``` - -### Bonus D. Link toevoegen in layout - -**Vertel:** "Nog even een link in de navbar. Open `layout.tsx` en voeg toe naast de Home link:" - -```tsx - - Nieuwe Poll - -``` - -### Bonus E. Testen! - -**Test:** localhost:3000/create β†’ vul een vraag in β†’ voeg opties toe β†’ klik aanmaken β†’ redirect naar home β†’ nieuwe poll staat er! - -**Vertel:** "Klaar! De hele app is compleet. Je kunt polls bekijken, stemmen, en nieuwe polls aanmaken." - ---- - -## βœ… HELEMAAL KLAAR! - -**Samenvatting wat we gebouwd hebben:** -1. Types & data (TypeScript interfaces, in-memory database) -2. Layout & homepage (Server Component, Link navigatie) -3. GET API route (folder = URL, dynamic routes) -4. POST API route (stemmen, validatie, status codes) -5. Poll detail pagina (generateMetadata, notFound) -6. VoteForm (Client Component, useState, fetch, animaties) -7. Loading, Error, Not-Found (speciale bestanden) -8. Middleware (request interceptie) -9. Bonus: Create poll (form, POST, redirect) - -**De Next.js flow:** -Route (folder) β†’ Page (server) β†’ Client Component (interactie) β†’ API Route (data) β†’ Response - -**Volgende les:** Tailwind CSS & shadcn/ui diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Slide-Overzicht.md b/Les06-NextJS-QuickPoll-Part2 2/Les06-Slide-Overzicht.md deleted file mode 100644 index 0c80161..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/Les06-Slide-Overzicht.md +++ /dev/null @@ -1,744 +0,0 @@ -# Les 6: Next.js β€” QuickPoll Compleet - Slide Overzicht - -> Versie 3 β€” **Van scratch tot werkende app in één les.** Geen apart theorieblok. -> -> Stap 0-3 = recap (sneller, studenten hebben dit al gezien in Les 5). -> Stap 4-8 = nieuw materiaal. -> Theorie wordt uitgelegd **op het moment dat we het tegenkomen.** - ---- - -## Slide 1: Titel + Plan - -### Op de Slide -- Titel: **Les 6: QuickPoll β€” Van Scratch tot Werkend** -- Subtitel: "De hele app, stap voor stap" -- **Les 6 van 18** -- Next.js logo - -- **Plan vandaag:** - - Stap 0-3: Project opzetten, types, layout, homepage, GET route *(recap)* - - Stap 4: POST vote route *(nieuw)* - - Stap 5: Poll detail pagina *(nieuw)* - - Stap 6: VoteForm component *(nieuw)* - - Stap 7: Loading, Error & Not-Found *(nieuw)* - - Stap 8: Middleware *(nieuw)* - -### Docentnotities -"Welkom terug! Vorige les hebben jullie kennis gemaakt met Next.js. Vandaag bouwen we de hele QuickPoll app van scratch tot werkend. Ik code, jullie volgen mee." - -"Stap 0-3 kennen jullie al β€” dat gaat lekker snel. Stap 4-8 is nieuw, daar nemen we de tijd voor. Laten we beginnen." - ---- - -## Slide 2: Stap 0 β€” Project Aanmaken - -### Op de Slide -- **Terminal:** -```bash -npx create-next-app@latest quickpoll -``` -- **Opties:** - - TypeScript? β†’ **Yes** - - ESLint? β†’ **Yes** - - Tailwind CSS? β†’ **Yes** - - `src/` directory? β†’ **Yes** - - App Router? β†’ **Yes** - - Import alias? β†’ **@/*** - -```bash -cd quickpoll -npm run dev -``` - -- **Check:** `localhost:3000` β†’ Next.js standaard pagina - -### Docentnotities -*Tim opent terminal, typt het commando.* - -"Stap 0 kennen jullie. `create-next-app` met TypeScript, Tailwind en App Router. Selecteer dezelfde opties als op het scherm." - -*Tim wacht tot iedereen `npm run dev` draait.* - -"Zie je de Next.js pagina? Dan gaan we door." - ---- - -## Slide 3: Stap 1 β€” Types & Data - -### Op de Slide -- **Maak `src/types/index.ts`:** - -```tsx -export interface Poll { - id: string; - question: string; - options: string[]; - votes: number[]; -} -``` - -- **Maak `src/lib/data.ts`:** - -```tsx -import type { Poll } from "@/types"; - -let polls: Poll[] = [ - { - id: "1", - question: "Wat is je favoriete programmeer taal?", - options: ["JavaScript", "Python", "TypeScript", "Rust"], - votes: [45, 32, 28, 15], - }, - { - id: "2", - question: "Hoe veel uur slaap krijg je per nacht?", - options: ["< 6 uur", "6-8 uur", "8+ uur"], - votes: [12, 68, 35], - }, - { - id: "3", - question: "Welke framework gebruik je het meest?", - options: ["React", "Vue", "Svelte", "Angular"], - votes: [89, 34, 12, 8], - }, -]; - -let nextId = 4; - -export function getPolls(): Poll[] { - return polls; -} - -export function getPollById(id: string): Poll | undefined { - return polls.find((poll) => poll.id === id); -} - -export function votePoll(pollId: string, optionIndex: number): Poll | undefined { - const poll = polls.find((p) => p.id === pollId); - if (!poll || optionIndex < 0 || optionIndex >= poll.options.length) return undefined; - poll.votes[optionIndex]++; - return poll; -} -``` - -### Docentnotities -**πŸ’‘ Theorie-moment: TypeScript interfaces in Next.js** - -"Stap 1: de basis. Een `Poll` interface en in-memory data. Herkennen jullie dit van Les 4?" - -*Tim typt de interface.* - -"Een poll heeft een id, vraag, opties, en stemmen. Die `votes` array loopt parallel met `options` β€” index 0 van votes hoort bij index 0 van options." - -*Tim typt data.ts.* - -"Dit is onze 'database' β€” gewoon een array in het geheugen. `getPolls`, `getPollById`, `votePoll`. Straks bij de Supabase lessen vervangen we dit door een echte database." - -*Checkpoint: "Heeft iedereen beide bestanden?"* - ---- - -## Slide 4: Stap 2 β€” Layout & Homepage - -### Op de Slide -- **Vervang `src/app/layout.tsx`:** - -```tsx -import type { Metadata } from "next"; -import Link from "next/link"; -import "./globals.css"; - -export const metadata: Metadata = { - title: "QuickPoll", - description: "Een snelle polling app met Next.js", -}; - -export default function RootLayout({ - children, -}: Readonly<{ children: React.ReactNode }>) { - return ( - - - -
{children}
-
-
- Β© 2026 QuickPoll β€” Built with Next.js 15 -
-
- - - ); -} -``` - -- **`Link`** β€” client-side navigatie (geen page reload) -- **`metadata`** β€” SEO titel & beschrijving -- **`{children}`** β€” hier komt de pagina-inhoud - -### Docentnotities -**πŸ’‘ Theorie-moment: Layout & Link** - -"Stap 2: de layout. Dit bestand wrapt ELKE pagina in je app. De navbar en footer staan hier β€” die veranderen nooit." - -"`Link` is de Next.js versie van `
`. Het verschil: Link navigeert client-side β€” geen page reload, veel sneller." - -"`metadata` is voor SEO. Kijk in je browser tab: 'QuickPoll'." - -*Tim wacht even, dan door naar de homepage.* - ---- - -## Slide 5: Stap 2 vervolg β€” Homepage - -### Op de Slide -- **Vervang `src/app/page.tsx`:** - -```tsx -import Link from "next/link"; -import { getPolls } from "@/lib/data"; - -export default function Home() { - const polls = getPolls(); - - return ( -
-
-

QuickPoll

-

Kies een poll en stem af in een oogwenk

-
- -
- {polls.map((poll) => { - const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0); - return ( - -
-

- {poll.question} -

-
- {poll.options.length} opties - {totalVotes} stemmen -
-
- Stemmen β†’ -
-
- - ); - })} -
-
- ); -} -``` - -- **Dit is een Server Component** β€” geen `"use client"`, data direct ophalen -- **`.map()`** β€” render een card per poll -- **`.reduce()`** β€” tel totaal stemmen -- **Check:** `localhost:3000` β†’ 3 poll cards - -### Docentnotities -**πŸ’‘ Theorie-moment: Server Components** - -"Dit is een Server Component. Geen `'use client'` bovenaan β€” dus deze code draait op de server. Je kunt direct data ophalen, geen useEffect, geen loading state nodig." - -"De homepage haalt alle polls op en rendert een card per poll. De `.map()` loop kennen jullie uit JavaScript β€” zelfde verhaal, maar dan in JSX." - -*Checkpoint: "Zie je 3 cards op localhost:3000? Mooi."* - ---- - -## Slide 6: Stap 3 β€” GET API Route - -### Op de Slide -- **Maak `src/app/api/polls/[id]/route.ts`:** - -```tsx -import { NextResponse } from "next/server"; -import { getPollById } from "@/lib/data"; - -export async function GET( - request: Request, - { params }: { params: Promise<{ id: string }> } -) { - const { id } = await params; - const poll = getPollById(id); - - if (!poll) { - return NextResponse.json({ error: "Poll not found" }, { status: 404 }); - } - - return NextResponse.json(poll); -} -``` - -- **Folder = URL:** `api/polls/[id]/route.ts` β†’ `/api/polls/1` -- **`[id]`** β€” dynamic route parameter -- **`await params`** β€” Next.js 15 pattern (params is een Promise) -- **`NextResponse.json()`** β€” stuur JSON response -- **Test:** `localhost:3000/api/polls/1` β†’ JSON in browser - -### Docentnotities -**πŸ’‘ Theorie-moment: API Routes & Dynamic Routes** - -"Stap 3: onze eerste API route. In Next.js is de folder-structuur je URL. `api/polls/[id]/route.ts` wordt `/api/polls/1`." - -"Die `[id]` met vierkante haakjes is een dynamic route. Het getal in de URL wordt de `id` parameter." - -"Let op: `await params` β€” in Next.js 15 zijn params een Promise. Vergeet de `await` niet, anders werkt het niet." - -*Tim opent browser, gaat naar localhost:3000/api/polls/1.* - -"JSON! Onze API werkt. Probeer `/api/polls/999` β€” daar krijg je een 404 error." - -*Checkpoint: "Werkt je API? Dan zijn we klaar met de recap. Nu het nieuwe werk."* - ---- - -## Slide 7: Stap 4 β€” POST /api/polls/[id]/vote - -### Op de Slide -- **Maak `src/app/api/polls/[id]/vote/route.ts`:** - -```tsx -import { NextResponse } from "next/server"; -import { votePoll } from "@/lib/data"; - -interface RouteParams { - params: Promise<{ id: string }>; -} - -interface VoteBody { - optionIndex: number; -} - -export async function POST( - request: Request, - { params }: RouteParams -): Promise { - const { id } = await params; - const body: VoteBody = await request.json(); - - if (typeof body.optionIndex !== "number") { - return NextResponse.json( - { error: "optionIndex is verplicht" }, - { status: 400 } - ); - } - - const updatedPoll = votePoll(id, body.optionIndex); - - if (!updatedPoll) { - return NextResponse.json( - { error: "Poll niet gevonden of ongeldige optie" }, - { status: 404 } - ); - } - - return NextResponse.json(updatedPoll); -} -``` - -- **Test met browser console:** -```javascript -fetch('/api/polls/1/vote', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ optionIndex: 0 }) -}).then(r => r.json()).then(console.log) -``` - -### Docentnotities -**πŸ’‘ Theorie-moment: POST vs GET** - -"Nu begint het nieuwe werk. GET = data ophalen, POST = data wijzigen. Vijf stappen in elke POST route: params, body, validatie, actie, response." - -*Tim typt langzaam, stopt na elke sectie.* - -"`request.json()` leest de body β€” wat de client meestuurt. Hier de `optionIndex`: welke optie gestemd is." - -"Twee error checks: 400 als de data ongeldig is, 404 als de poll niet bestaat. Altijd beide afvangen." - -*Tim opent console, test de fetch.* - -"Plak dit in je console. Zie je? Votes veranderd. Onze stem-API werkt." - -*Checkpoint: "Heeft iedereen een JSON resultaat?"* - ---- - -## Slide 8: Stap 5 β€” Poll Detail Pagina - -### Op de Slide -- **Maak `src/app/poll/[id]/page.tsx`:** - -```tsx -import { notFound } from "next/navigation"; -import { getPollById } from "@/lib/data"; -import VoteForm from "@/components/VoteForm"; -import type { Metadata } from "next"; - -interface PageProps { - params: Promise<{ id: string }>; -} - -export async function generateMetadata({ params }: PageProps): Promise { - const { id } = await params; - const poll = getPollById(id); - if (!poll) return { title: "Poll niet gevonden" }; - return { - title: `${poll.question} β€” QuickPoll`, - description: `Stem op: ${poll.options.join(", ")}`, - }; -} - -export default async function PollPage({ params }: PageProps) { - const { id } = await params; - const poll = getPollById(id); - - if (!poll) { - notFound(); - } - - return ( -
-

- {poll.question} -

- -
- ); -} -``` - -- **`generateMetadata`** β€” dynamische titel per poll in browser tab -- **`notFound()`** β€” toont 404 als poll niet bestaat -- **Server Component** die Client Component (`VoteForm`) rendert - -### Docentnotities -**πŸ’‘ Theorie-moment: generateMetadata & notFound** - -"`generateMetadata`: elke poll krijgt z'n eigen titel. Open straks `/poll/1` en kijk naar je browser tab." - -"`notFound()`: als de poll niet bestaat, roep je dit aan. Next.js toont dan automatisch een 404 pagina. Geen if/else redirect nodig." - -"En kijk: Server Component rendert een Client Component. Server haalt data, client doet interactie. Dat is de kern van Next.js." - -*Checkpoint: "/poll/1 geeft een error β€” VoteForm bestaat nog niet. Dat klopt."* - ---- - -## Slide 9: Pauze β˜• - -### Op de Slide -- **PAUZE β€” 15 minuten** -- Check: stap 0-5 af? Alles draait? -- Na de pauze: VoteForm (het leukste stuk) - -### Docentnotities -"Pauze! We zijn halverwege. Stap 0-5 staan er. Na de pauze bouwen we de VoteForm β€” dat is het interactieve hart van de app." - -*Tim loopt rond, helpt waar nodig.* - ---- - -## Slide 10: Stap 6 β€” VoteForm (Deel 1: Logica) - -### Op de Slide -- **Maak `src/components/VoteForm.tsx`:** - -```tsx -"use client"; - -import { useState } from "react"; -import type { Poll } from "@/types"; - -interface VoteFormProps { - poll: Poll; -} - -export default function VoteForm({ poll }: VoteFormProps) { - const [selectedOption, setSelectedOption] = useState(null); - const [hasVoted, setHasVoted] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const [currentPoll, setCurrentPoll] = useState(poll); - - const totalVotes: number = currentPoll.votes.reduce( - (sum, v) => sum + v, 0 - ); - - function getPercentage(votes: number): number { - if (totalVotes === 0) return 0; - return Math.round((votes / totalVotes) * 100); - } - - async function handleVote(): Promise { - if (selectedOption === null || isSubmitting) return; - setIsSubmitting(true); - - const response = await fetch(`/api/polls/${currentPoll.id}/vote`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ optionIndex: selectedOption }), - }); - - if (response.ok) { - const updatedPoll: Poll = await response.json(); - setCurrentPoll(updatedPoll); - setHasVoted(true); - } - - setIsSubmitting(false); - } - - // UI op volgende slide... -``` - -- **`"use client"`** β€” dit component draait in de browser -- **4 state variables** met TypeScript types -- **`handleVote()`** β€” POST naar de API route uit stap 4 - -### Docentnotities -**πŸ’‘ Theorie-moment: Client Components & useState** - -"Dit is het hart van de app. `'use client'` bovenaan β€” alles met interactiviteit moet een Client Component zijn." - -"Vier states: welke optie geselecteerd (`number | null`), al gestemd, bezig met submitten, en de huidige poll data." - -"`handleVote` is dezelfde fetch als we in stap 4 in de console testten. Nu zit het in een component." - -*Checkpoint: "Heeft iedereen de vier useState regels en de handleVote functie?"* - ---- - -## Slide 11: Stap 6 β€” VoteForm (Deel 2: UI) - -### Op de Slide -```tsx - return ( -
- {currentPoll.options.map((option, index) => { - const percentage = getPercentage(currentPoll.votes[index]); - const isSelected = selectedOption === index; - - return ( - - ); - })} - - {!hasVoted && ( - - )} - - {hasVoted && ( -

- Bedankt voor je stem! Totaal: {totalVotes} stemmen -

- )} -
- ); -} -``` - -- **Twee UI states:** vΓ³Γ³r stemmen (selecteer) en na stemmen (percentage bars) -- **Percentage bar:** `absolute inset-0` + `width: percentage%` + `transition-all` -- **Conditional classes:** ternary in className -- **Test:** `localhost:3000/poll/1` β†’ stem en zie de animatie! - -### Docentnotities -"De UI heeft twee toestanden. VΓ³Γ³r stemmen: klik een optie, paarse border. Na stemmen: geanimeerde percentage bars." - -"De percentage bar: een div die de achtergrond vult. De breedte is het percentage. `transition-all duration-500` animeert het. Pure CSS via Tailwind." - -*Tim opent /poll/1, klikt een optie, stemt.* - -"Werkt het? Klik op een optie, klik Stem. Zie je de animatie?" - -*Tim loopt rond. Dit is het lastigste stuk β€” neem 10-15 min extra als nodig.* - ---- - -## Slide 12: Stap 7 β€” Loading, Error & Not-Found - -### Op de Slide -**`src/app/loading.tsx`:** -```tsx -export default function Loading() { - return ( -
-
-
-
-
- {[1, 2, 3].map((i) => ( -
-
-
-
- ))} -
- ); -} -``` - -**`src/app/error.tsx`** *(let op: `"use client"` verplicht!)*: -```tsx -"use client"; -export default function Error({ error, reset }: { error: Error; reset: () => void }) { - return ( -
-

Er ging iets mis!

-

{error.message}

- -
- ); -} -``` - -**`src/app/not-found.tsx`:** -```tsx -import Link from "next/link"; -export default function NotFound() { - return ( -
-

404

-

Deze pagina bestaat niet.

- Terug naar home -
- ); -} -``` - -- **Test:** `/poll/999` β†’ not-found pagina - -### Docentnotities -**πŸ’‘ Theorie-moment: Speciale bestanden** - -"In gewoon React bouw je dit zelf. In Next.js maak je een bestand aan en het werkt." - -"Drie bestanden: `loading.tsx` met skeleton UI (`animate-pulse`), `error.tsx` als error boundary (**altijd `'use client'`!**), en `not-found.tsx` voor 404's." - -"Test: ga naar `/poll/999`. Daar is je 404. Dat werkt door `notFound()` in de poll pagina." - ---- - -## Slide 13: Stap 8 β€” Middleware - -### Op de Slide -- **Maak `src/middleware.ts`** *(in src/ root, NIET in een folder!)*: - -```tsx -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export function middleware(request: NextRequest): NextResponse { - const start = Date.now(); - console.log(`[${request.method}] ${request.nextUrl.pathname}`); - - const response = NextResponse.next(); - response.headers.set("x-request-time", String(Date.now() - start)); - return response; -} - -export const config = { - matcher: ["/api/:path*", "/poll/:path*"], -}; -``` - -- **Locatie:** altijd `src/middleware.ts` β€” nergens anders -- **`matcher`** β€” welke routes de middleware triggert -- **Test:** terminal logs + DevTools Headers β†’ `x-request-time` - -### Docentnotities -**πŸ’‘ Theorie-moment: Middleware** - -"Middleware is een portier β€” elke request passeert hier eerst. Nu loggen we alleen, maar bij Supabase wordt dit authenticatie: is de user ingelogd?" - -"Let op de locatie: `src/middleware.ts`. Niet in `app/`, niet in een subfolder. Dit is het enige bestand in Next.js waar de locatie echt uitmaakt." - -"Open je terminal. Klik op een poll. Zie je `[GET] /poll/1`? Dat is middleware." - ---- - -## Slide 14: Huiswerk & Afsluiting - -### Op de Slide -- **Wat we gebouwd hebben:** De hele QuickPoll app van scratch - - Types & in-memory data - - Layout, homepage, API routes (GET + POST) - - Poll detail pagina met dynamic metadata - - VoteForm met interactieve UI - - Loading, Error, Not-Found states - - Middleware - -- **De Next.js flow:** - Route (folder) β†’ Page (server) β†’ Client Component (interactie) β†’ API Route (data) β†’ Response - -- **Huiswerk:** - - App niet af? β†’ Afmaken - - App af? β†’ Bonus: "Nieuwe Poll Aanmaken" pagina (`/create`) - - Zet je code op GitHub - -- **Volgende les:** Tailwind CSS & shadcn/ui - -### Docentnotities -"In één les van nul naar werkende app. Dat is Next.js. Routing, server components, client components, API routes, middleware β€” jullie snappen de kern." - -"Bonus: maak een `/create` pagina met een form en POST naar `/api/polls`. Goed oefenmateriaal." - -"Volgende les: styling met Tailwind en shadcn/ui." - -"Goed gedaan!" diff --git a/Les06-NextJS-QuickPoll-Part2 2/Les06-Slides.pptx b/Les06-NextJS-QuickPoll-Part2 2/Les06-Slides.pptx deleted file mode 100644 index 55252d0..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/Les06-Slides.pptx and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/INDEX.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/INDEX.md deleted file mode 100644 index b885ac2..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/INDEX.md +++ /dev/null @@ -1,336 +0,0 @@ -# QuickPoll Next.js Demo - Complete File Index - -## πŸ“ START HERE - -1. **For Tim (Teacher)**: Read `QUICK-START.md` first -2. **For Understanding the Project**: Read `README.md` -3. **For Complete Teaching Guide**: Read `TEACHER-GUIDE.md` -4. **For Technical Details**: Read `PROJECT-SUMMARY.md` - ---- - -## πŸ“¦ Project Files - -### Complete Project with Git History -``` -quickpoll-demo-complete.zip ~160 KB -└─ Full project with all 10 git tags - Extract this and use git checkout to jump between steps -``` - -### Individual Step Archives -``` -stap-0.zip ~32 KB Project setup + types + data -stap-1.zip ~32 KB Layout & Navigation -stap-2.zip ~33 KB Homepage with poll cards -stap-3.zip ~34 KB API routes (GET/POST) -stap-4.zip ~37 KB Vote API + demo form ⭐ -stap-5.zip ~39 KB Poll detail page -stap-6.zip ~40 KB VoteForm component -stap-7.zip ~43 KB Error handling pages -stap-8.zip ~43 KB Middleware -bonus.zip ~44 KB Create poll page -``` - -**Total Project Size**: ~588 KB (all files) - ---- - -## πŸ“š Documentation - -### Quick Start (3 pages, 7.1 KB) -File: `QUICK-START.md` -- For Tim before class -- Setup instructions -- Key demo points -- Troubleshooting -- Recommended read time: 10 minutes - -### Main README (3 pages, 7.2 KB) -File: `README.md` -- Project overview -- Getting started -- Key features -- Technology stack -- Common questions - -### Complete Teacher Guide (5 pages, 8.5 KB) -File: `TEACHER-GUIDE.md` -- Complete lesson breakdown -- What to show in each step -- DevTools demonstration strategy -- Key concepts to teach -- Tips for live coding - -### Project Summary (4 pages, 7.9 KB) -File: `PROJECT-SUMMARY.md` -- Technical architecture -- Git tag progression -- Teaching points -- Sample data -- Build information - ---- - -## πŸŽ“ Learning Path - -### Beginner Path (Individual Zips) -If you want to start from scratch for each step: -``` -1. Extract stap-0.zip -2. npm install && npm run dev -3. Do the live-coding -4. Extract stap-1.zip (new terminal) -5. Repeat... -``` - -**Pros**: Each step is fresh, you can code along -**Cons**: More setup, can't jump back easily - -### Advanced Path (Complete Project) -If you want flexible jumping between steps: -``` -1. Extract quickpoll-demo-complete.zip -2. npm install -3. npm run dev -4. git checkout stap-0 (or any step) -5. Browser auto-refreshes -6. git checkout stap-5 (jump forward) -``` - -**Pros**: One setup, jump anywhere, see git history -**Cons**: Less live-coding from scratch - -**Recommended**: Use Complete Project with git checkout - ---- - -## πŸ“‹ File Structure in Zips - -Each zip contains a Next.js project with this structure: - -``` -quickpoll-demo/ -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ app/ # Next.js App Router -β”‚ β”‚ β”œβ”€β”€ page.tsx # Homepage -β”‚ β”‚ β”œβ”€β”€ layout.tsx # Root layout with navbar -β”‚ β”‚ β”œβ”€β”€ globals.css # Tailwind imports -β”‚ β”‚ β”œβ”€β”€ loading.tsx # Loading skeleton -β”‚ β”‚ β”œβ”€β”€ error.tsx # Error boundary -β”‚ β”‚ β”œβ”€β”€ not-found.tsx # 404 page -β”‚ β”‚ β”œβ”€β”€ api/ -β”‚ β”‚ β”‚ └── polls/ -β”‚ β”‚ β”‚ β”œβ”€β”€ route.ts # GET all, POST create -β”‚ β”‚ β”‚ └── [id]/ -β”‚ β”‚ β”‚ β”œβ”€β”€ route.ts # GET single poll -β”‚ β”‚ β”‚ β”œβ”€β”€ vote/route.ts # POST vote ⭐ -β”‚ β”‚ β”‚ └── not-found.tsx -β”‚ β”‚ β”œβ”€β”€ poll/[id]/ -β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # Dynamic detail page -β”‚ β”‚ β”‚ └── not-found.tsx -β”‚ β”‚ └── create/ -β”‚ β”‚ └── page.tsx # Create poll form (bonus) -β”‚ β”œβ”€β”€ components/ -β”‚ β”‚ └── VoteForm.tsx # Voting component -β”‚ β”œβ”€β”€ lib/ -β”‚ β”‚ └── data.ts # Mock data & functions -β”‚ β”œβ”€β”€ types/ -β”‚ β”‚ └── index.ts # TypeScript interfaces -β”‚ └── middleware.ts # Request logging -β”œβ”€β”€ .cursorrules # Code style guidelines -β”œβ”€β”€ package.json -β”œβ”€β”€ tsconfig.json -β”œβ”€β”€ tailwind.config.ts -└── .gitignore -``` - ---- - -## ⭐ Key Files to Show Students - -### Data & Types -- `src/types/index.ts` - Poll interface, VoteBody interface -- `src/lib/data.ts` - Sample polls, getPolls(), votePoll() - -### API Routes -- `src/app/api/polls/route.ts` - GET all, POST create -- `src/app/api/polls/[id]/route.ts` - GET single -- `src/app/api/polls/[id]/vote/route.ts` - POST vote (Key demo point!) - -### Pages -- `src/app/page.tsx` - Homepage with poll cards -- `src/app/poll/[id]/page.tsx` - Poll detail page with voting - -### Components -- `src/components/VoteForm.tsx` - Client component with voting logic - -### Layout & Styling -- `src/app/layout.tsx` - Navbar and global structure -- `src/app/globals.css` - Tailwind imports (minimal) - ---- - -## πŸ”§ System Requirements - -- **Node.js**: 18 or higher -- **npm**: 9 or higher -- **Disk Space**: ~500 MB for node_modules (generated during npm install) -- **Browser**: Any modern browser with DevTools (for demos) -- **Projector**: Recommended for classroom (test beforehand!) - ---- - -## πŸš€ Quick Commands Reference - -### Setup -```bash -unzip quickpoll-demo-complete.zip -cd quickpoll-demo -npm install -npm run dev -``` - -### During Class -```bash -git tag -l # List all tags -git checkout stap-4 # Jump to step 4 -git diff stap-2 stap-3 # See what changed -git log --oneline # Show commits -``` - -### Build & Test -```bash -npm run build # Production build -npm run dev -- -p 3001 # Different port -``` - -### Browser -``` -http://localhost:3000 # Homepage -http://localhost:3000/poll/1 # Poll detail -http://localhost:3000/create # Create form (bonus) -http://localhost:3000/demo # Demo form (stap-4 only) -``` - -### DevTools (F12) -- Console: See console.log statements -- Network: See API requests -- Elements: Inspect HTML/CSS - ---- - -## πŸ“Š Project Statistics - -| Metric | Value | -|--------|-------| -| Git Commits | 10 | -| Git Tags | 9 (stap-0 through stap-8, plus bonus) | -| TypeScript Files | 11 | -| Components | 2 (VoteForm + pages) | -| API Routes | 4 | -| Total Lines of Code | ~1,200 | -| Build Time | ~1.3 seconds | -| Estimated Teaching Time | 60-75 minutes | - ---- - -## βœ… Verification Checklist - -Before teaching, verify: - -- [ ] All 10 zip files present and readable -- [ ] quickpoll-demo-complete.zip contains .git folder -- [ ] All 4 markdown documentation files present -- [ ] npm version 9+ installed -- [ ] Node 18+ installed -- [ ] Can extract and npm install without errors -- [ ] npm run dev starts without errors -- [ ] Homepage loads at http://localhost:3000 -- [ ] All git tags present (`git tag -l`) -- [ ] Can checkout different tags without errors -- [ ] DevTools work correctly (F12) -- [ ] Network tab shows API requests -- [ ] Console.log appears in DevTools console - ---- - -## 🎯 Recommended Usage - -### For First-Time Teachers -1. Read QUICK-START.md (10 min) -2. Follow setup instructions -3. Test each step beforehand -4. Use git checkout to jump between steps -5. Use projector for display - -### For Experienced Teachers -1. Extract complete project -2. Review TEACHER-GUIDE.md for key points -3. Customize as needed -4. Focus on stap-4 demo form - -### For Students (After Class) -1. Extract any zip file -2. Run npm install && npm run dev -3. Explore the code -4. Try modifying things -5. See changes in real-time - ---- - -## πŸ†˜ Common Issues - -| Problem | Solution | -|---------|----------| -| "npm: command not found" | Install Node.js from nodejs.org | -| "Port 3000 in use" | Use `npm run dev -- -p 3001` | -| Git tags not showing | You extracted individual zip, not complete | -| Changes not showing | Hard refresh browser: Ctrl+Shift+R | -| node_modules missing | Run `npm install` | -| TypeScript errors | All should build successfully | - ---- - -## πŸ“ž File Organization - -All files are in one directory: -``` -/demo-output/ -β”œβ”€β”€ (4 markdown files - documentation) -β”œβ”€β”€ (10 zip files - project archives) -└── (this index file) -``` - -For best organization, create this structure: -``` -QuickPoll-Demo/ -β”œβ”€β”€ Zips/ -β”‚ β”œβ”€β”€ stap-0.zip through stap-8.zip -β”‚ β”œβ”€β”€ bonus.zip -β”‚ └── quickpoll-demo-complete.zip -β”œβ”€β”€ Documentation/ -β”‚ β”œβ”€β”€ QUICK-START.md -β”‚ β”œβ”€β”€ TEACHER-GUIDE.md -β”‚ β”œβ”€β”€ PROJECT-SUMMARY.md -β”‚ β”œβ”€β”€ README.md -β”‚ └── INDEX.md -└── Projects/ - └── (extract zips here as needed) -``` - ---- - -## πŸŽ‰ You're Ready! - -Everything is prepared for live-coding demonstrations. Start with QUICK-START.md and you'll have a successful classroom session. - -Good luck teaching Next.js! πŸš€ - ---- - -**Created**: March 17, 2026 -**Files**: 15 total (4 docs + 11 zips) -**Total Size**: 588 KB -**Format**: Standard zip archives + Markdown -**Compatibility**: All platforms (Windows, Mac, Linux) diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/QUICK-START.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/QUICK-START.md deleted file mode 100644 index 12a83b4..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/QUICK-START.md +++ /dev/null @@ -1,304 +0,0 @@ -# QuickPoll Demo - Quick Start Guide - -## For Tim - Before Your Class - -### 1️⃣ Prepare Your Setup (30 minutes before class) - -```bash -# Extract the complete project -unzip quickpoll-demo-complete.zip - -# Enter directory -cd quickpoll-demo - -# Install dependencies -npm install - -# Test that it runs -npm run dev -``` - -Should see: -``` -- ready started server on 0.0.0.0:3000, url: http://localhost:3000 -``` - -### 2️⃣ Check All Git Tags Are Present - -```bash -git tag -l -``` - -You should see: -``` -bonus -stap-0 -stap-1 -stap-2 -stap-3 -stap-4 -stap-5 -stap-6 -stap-7 -stap-8 -``` - -### 3️⃣ Test Jumping Between Steps - -```bash -# Go to step 2 -git checkout stap-2 - -# Refresh browser - should show just poll cards -# Look at src/app/page.tsx - should be simple - -# Go to final version -git checkout bonus - -# Refresh browser - should show full app with "Nieuwe Poll" link -# Look at src/app/create/page.tsx - should exist -``` - -### 4️⃣ Check Your Display Setup - -- Open browser with http://localhost:3000 -- Test font size on projector (should be easily readable) -- Test network tab in DevTools (for stap-4 demo) -- Test VS Code has good contrast - ---- - -## During Your Class - -### Starting the Session - -```bash -# Make sure you're at the beginning -git checkout stap-0 -npm run dev -``` - -Then open http://localhost:3000 in your browser. - -### Jumping Between Steps - -When you want to show a new step: - -```bash -# Stop the dev server (Ctrl+C) -git checkout stap-3 - -# Restart dev server -npm run dev - -# Browser should hot-reload automatically -# If not, refresh manually (Ctrl+R or Cmd+R) -``` - -### Key Demo Points - -#### ⭐ Step 4 - The Voting API Demo (Most Important) - -1. Navigate to http://localhost:3000/demo -2. Open DevTools (F12) -3. Go to Console tab - show logging -4. Go to Network tab -5. Fill the form with: - - Poll ID: `1` - - Option Index: `0` -6. Click "Stem" button -7. Watch the Network tab to show POST request -8. Show the JSON response -9. Show console logs - -This is your most visual demonstration! - -#### Step 1 - Layout - -Show the navbar: -- Point out the "QuickPoll" branding (purple) -- Show the links being added - -#### Step 2 - Homepage - -Show poll cards appearing: -- Click one to navigate to detail page -- Show responsive grid - -#### Step 3 - API Routes - -Show file structure in VS Code: -- `src/app/api/polls/route.ts` - GET all, POST create -- `src/app/api/polls/[id]/route.ts` - GET single -- Point out `params: Promise<{ id: string }>` - Next.js 15 pattern - -#### Step 5-6 - Poll Detail & Voting - -Show the full flow: -1. Homepage β†’ Click a poll -2. Vote on the poll -3. See results appear -4. Show purple gradient progress bars - -#### Step 7-8 - Error Handling & Middleware - -Show what happens when things break: -- Try navigating to non-existent poll (404 page) -- Point out loading skeleton briefly -- Show middleware logging in terminal - -### Stopping the Dev Server - -```bash -Ctrl+C -``` - ---- - -## Troubleshooting During Class - -| Problem | Solution | -|---------|----------| -| Port 3000 in use | `npm run dev -- -p 3001` | -| Browser shows old code | Hard refresh: `Ctrl+Shift+R` or `Cmd+Shift+R` | -| Git checkout fails | Run `git status` to check for uncommitted changes, then `git stash` | -| DevTools won't show Network tab | Close and reopen DevTools (F12) | -| Terminal shows errors | Try: `npm install` then `npm run dev` again | -| Can't see projector clearly | Zoom in: `Ctrl/Cmd + +` in browser | - ---- - -## File Locations You'll Reference - -``` -quickpoll-demo/ -β”œβ”€β”€ src/types/index.ts ← Show for TypeScript interfaces -β”œβ”€β”€ src/lib/data.ts ← Show for sample data -β”œβ”€β”€ src/app/layout.tsx ← Show for navbar -β”œβ”€β”€ src/app/page.tsx ← Show for homepage -β”œβ”€β”€ src/app/api/polls/ ← Show for API routes -β”œβ”€β”€ src/app/poll/[id]/page.tsx ← Show for dynamic routing -β”œβ”€β”€ src/components/VoteForm.tsx ← Show for client component -β”œβ”€β”€ src/app/create/page.tsx ← Show for form handling -└── src/middleware.ts ← Show for middleware concept -``` - ---- - -## Live-Coding Tips - -### Before You Type -- Take a screenshot for reference -- Know where you're going -- Have keyboard shortcuts ready - -### While You Type -- Type **slowly** - let students follow -- **Talk through** what you're doing -- **Pause** at key lines to explain -- **Ask questions** - make it interactive - -### Show Results -- Run frequently -- Show in browser on projector -- Use DevTools to demonstrate -- Let students ask questions - ---- - -## Sample Dialog When Showing Step 4 - -> "Okay, we have our API route that accepts votes. But how does it work? Let me show you with this demo form." -> -> *Navigate to /demo* -> -> "Here we have a simple form. Let me open DevTools to show what happens behind the scenes." -> -> *Open DevTools, go to Console tab* -> -> "When I click 'Stem', two things happen: -> 1. The browser makes a request to our API -> 2. We get back the updated poll with new vote counts" -> -> *Click "Stem" button* -> -> "Look at the Console - you can see exactly what happened. Now let me show you in the Network tab..." -> -> *Go to Network tab* -> -> "Here's our POST request. The body shows we sent the option index (0), and the response shows the entire poll with updated vote counts." - ---- - -## After Class - -### Save Your Progress -The project uses git, so all your changes are tracked. If you made modifications, you can: - -```bash -# See what you changed -git status - -# Commit your changes -git add . -git commit -m "My live-coding edits" -``` - -### For Student Assignments -You can: -1. Share the `stap-5.zip` as a starting point -2. Have students add the VoteForm (stap-6 solution) -3. Or start with complete project and have them add features - -### Deploy (Optional) -To deploy this app: -```bash -# Sign up at vercel.com -# Install Vercel CLI -npm i -g vercel - -# Deploy -vercel -``` - ---- - -## Key Concepts to Emphasize - -βœ… **Server Components** - Default in Next.js, simpler, faster -βœ… **Client Components** - Use "use client" only when needed -βœ… **API Routes** - Backend code in `/api` folder -βœ… **Dynamic Routes** - `[id]` creates flexible routing -βœ… **TypeScript** - Catch errors before they happen -βœ… **Tailwind CSS** - Write styles in HTML with utility classes -βœ… **Fetch API** - How frontend talks to backend - ---- - -## Questions Students Will Ask - -**"Why use Next.js instead of just React?"** -> Next.js gives you Server Components, API routes, better file structure, and automatic optimization. React is just the UI library - Next.js is the complete framework. - -**"Can we use a database instead of in-memory data?"** -> Yes! Add PostgreSQL or MongoDB. That's another lesson though. - -**"How long would this take to build from scratch?"** -> About 1-2 hours for a developer. We're doing it in steps so you can understand each concept. - -**"Can we deploy this?"** -> Absolutely - to Vercel in 1 command. That's our bonus if we have time. - ---- - -## You've Got This! πŸŽ‰ - -The project is ready. You have: -- βœ… Complete, working Next.js 15 app -- βœ… Git tags at every step -- βœ… Demo form for showing APIs -- βœ… Clear progression from basic to advanced -- βœ… Purple theme ready for classroom -- βœ… Console logs for debugging - -Just follow this guide, and your students will love seeing a real app built step-by-step! diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/README.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/README.md deleted file mode 100644 index 1d26e7e..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/README.md +++ /dev/null @@ -1,278 +0,0 @@ -# QuickPoll Next.js 15 Demo Project - -A complete, step-by-step Next.js 15 polling application built for live-coding classroom demonstrations. Includes git tags at each development step for easy navigation during teaching. - -## πŸ“¦ What's Included - -### Zip Files (Individual Steps) -- `stap-0.zip` - Project setup + types + data -- `stap-1.zip` - Layout & Navigation -- `stap-2.zip` - Homepage with poll cards -- `stap-3.zip` - API routes (GET, POST) -- `stap-4.zip` - Vote API + demo form ⭐ -- `stap-5.zip` - Poll detail page -- `stap-6.zip` - VoteForm component -- `stap-7.zip` - Error handling & loading states -- `stap-8.zip` - Middleware -- `bonus.zip` - Create poll page - -### Complete Project -- `quickpoll-demo-complete.zip` - Full project with complete git history - -### Documentation -- `QUICK-START.md` - Instructions for Tim (start here!) -- `TEACHER-GUIDE.md` - Comprehensive teaching guide -- `PROJECT-SUMMARY.md` - Technical overview -- `README.md` - This file - -## πŸš€ Getting Started - -### Step 1: Extract & Setup -```bash -unzip quickpoll-demo-complete.zip -cd quickpoll-demo -npm install -npm run dev -``` - -### Step 2: Navigate to App -Open http://localhost:3000 in your browser - -### Step 3: Jump Between Steps (During Teaching) -```bash -# View all available steps -git tag -l - -# Jump to any step -git checkout stap-3 - -# See what changed in this step -git diff stap-2 stap-3 - -# Go to final version -git checkout bonus -``` - -## ✨ Key Features - -- **9 Development Steps** - Logical progression from setup to full app -- **API Routes** - Backend routes in `/app/api` -- **Dynamic Routing** - `/poll/[id]` for individual poll pages -- **Real-Time Voting** - Vote and see results update instantly -- **Server Components** - Default Next.js 15 pattern -- **Client Components** - Interactive VoteForm with state -- **Error Handling** - Loading skeletons, error boundaries, 404 pages -- **TypeScript** - Fully typed throughout -- **Tailwind CSS** - Purple-themed responsive design -- **Middleware** - Request logging and timing -- **Demo Form** - Testing interface for API endpoints (stap-4) - -## πŸ“š Documentation - -### For Teachers -Start with **QUICK-START.md** - has everything you need before class - -### For Understanding the Project -Read **PROJECT-SUMMARY.md** - complete technical overview - -### For Teaching Details -See **TEACHER-GUIDE.md** - tips, key concepts, troubleshooting - -## 🎯 Typical Class Flow - -| Step | Duration | Focus | -|------|----------|-------| -| stap-0 to stap-2 | 15 min | File structure, homepage, cards | -| stap-3 | 10 min | API routes concept | -| stap-4 | 15 min | ⭐ Vote API demo (show DevTools!) | -| stap-5 to stap-6 | 15 min | Dynamic pages, voting interface | -| stap-7 to stap-8 | 10 min | Error handling, middleware | -| bonus | 10 min | Form handling, creating polls | - -**Total: ~75 minutes for complete lesson** - -## πŸ’‘ Key Teaching Moments - -### stap-4: The Demo Form -This is your star moment! Show: -1. Demo form at http://localhost:3000/demo -2. Open DevTools Console - show logging -3. Switch to Network tab - show POST request -4. Click "Stem" and watch the request happen -5. Show the JSON response with updated votes - -### stap-6: VoteForm Component -Explain: -- Why "use client" is needed (state management) -- How useState tracks selected option -- How fetch() sends vote to API -- How component updates when response comes back - -### All Steps -Keep showing the running app - live feedback is key! - -## πŸ“‹ What's in Each Step - -### stap-0: Project Setup -- Create Next.js with TypeScript + Tailwind -- Define Poll interfaces -- Create mock data (3 sample polls) -- Set up folder structure - -### stap-1: Layout & Navigation -- Add navbar with branding -- Set up global layout -- Add metadata - -### stap-2: Homepage -- Display all polls as cards -- Show poll stats (option count, vote count) -- Add links to individual polls - -### stap-3: API Routes -- `GET /api/polls` - fetch all polls -- `GET /api/polls/[id]` - fetch single poll -- `POST /api/polls` - create new poll - -### stap-4: Vote API + Demo -- `POST /api/polls/[id]/vote` - record a vote -- Demo form at `/demo` for testing - -### stap-5: Poll Detail Page -- Dynamic route `/poll/[id]` -- Dynamic metadata for SEO -- Show poll questions and options - -### stap-6: VoteForm Component -- Client component with voting logic -- Real-time results with percentage bars -- Purple gradient progress bars - -### stap-7: Error Handling -- Loading skeleton (Suspense) -- Error boundary component -- 404 pages - -### stap-8: Middleware -- Request logging -- Timing middleware -- Route matching - -### bonus: Create Poll -- Form to create new polls -- Dynamic option inputs -- Form validation -- Navigation after creation - -## πŸ›  Technology Stack - -- **Next.js 15** - React framework -- **TypeScript** - Type-safe JavaScript -- **Tailwind CSS** - Utility-first CSS -- **React Hooks** - useState for interactivity -- **Fetch API** - For API calls -- **Git** - Version control with tags - -## 🎨 Design System - -**Purple Theme:** -- Primary: `#7c3aed` (purple-500) -- Hover: `#a855f7` (purple-600) -- Active: `#9333ea` (purple-700) - -**Neutral:** -- Light backgrounds: Gray-50 -- Text: Gray-900 -- Borders: Gray-200 -- Errors: Red-600 - -## βœ… Build & Deployment - -### Verify Build -```bash -npm run build -``` - -### Deploy to Vercel -```bash -npm i -g vercel -vercel -``` - -One command deployment! (You need a Vercel account) - -## πŸ“Œ Important Notes - -### In-Memory Data -This project uses in-memory JavaScript arrays for data. In production, you'd use: -- PostgreSQL -- MongoDB -- Supabase -- Firebase - -### No Authentication -This project doesn't have user accounts. Real apps would have: -- User authentication -- User-specific voting history -- Admin panels - -### No Database -Each time you restart the app, votes reset. That's fine for demos! - -## πŸ€” Common Questions - -**Q: Is this production-ready?** -A: No - it's designed for learning. Add a database for production. - -**Q: Can I modify it for my class?** -A: Yes! Use it as a starting point for assignments. - -**Q: How do I extend it?** -A: Add a real database (PostgreSQL with Prisma is popular), add authentication, add user accounts, etc. - -**Q: Where can students learn more?** -A: https://nextjs.org/learn - official Next.js tutorial - -## πŸ“ž Support - -Each zip file is independent and self-contained. You can: -1. Extract any `stap-X.zip` individually -2. Run `npm install` and `npm run dev` -3. See the app at that stage of development - -The `quickpoll-demo-complete.zip` has git history, so you can: -1. `git log --oneline` - see all commits -2. `git checkout stap-3` - jump to any step -3. `git diff stap-2 stap-3` - see what changed - -## πŸŽ“ For Students - -After the lesson, students can: -1. Clone the repo and explore the code -2. Modify styling with Tailwind -3. Add new features (poll deletion, editing, etc.) -4. Connect to a real database -5. Deploy to Vercel - -This is a great foundation for learning full-stack web development! - -## πŸ“ Notes - -- All code in English (variable names, comments) -- UI text in Dutch (button labels, messages) -- Fully typed TypeScript throughout -- Comprehensive comments for teaching -- Clean, readable code style - -## πŸŽ‰ Ready to Teach! - -You have everything you need. Good luck with your live-coding demonstration! - -Start with **QUICK-START.md** for immediate instructions. - ---- - -**Version:** 1.0 -**Next.js:** 15.x -**Node:** 18+ recommended -**Date Created:** March 2026 diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/TEACHER-GUIDE.md b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/TEACHER-GUIDE.md deleted file mode 100644 index 162fbc6..0000000 --- a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/TEACHER-GUIDE.md +++ /dev/null @@ -1,311 +0,0 @@ -# QuickPoll Next.js Demo - Live Coding Guide for Tim - -## Overview - -This is a complete, step-by-step Next.js 15 project built for live-coding demonstrations. Each step has a git tag, allowing you to jump between different stages of the application during class. - -## File Structure - -``` -demo-output/ -β”œβ”€β”€ stap-0.zip # Project setup + types + data -β”œβ”€β”€ stap-1.zip # Layout & Navigation -β”œβ”€β”€ stap-2.zip # Homepage with poll cards -β”œβ”€β”€ stap-3.zip # API routes (GET all, GET single, POST create) -β”œβ”€β”€ stap-4.zip # POST vote route + demo form -β”œβ”€β”€ stap-5.zip # Poll detail page -β”œβ”€β”€ stap-6.zip # VoteForm component -β”œβ”€β”€ stap-7.zip # Loading, error, not-found pages -β”œβ”€β”€ stap-8.zip # Middleware -β”œβ”€β”€ bonus.zip # Create poll page -└── quickpoll-demo-complete.zip # Full project with complete git history -``` - -## How to Use in Live-Coding Class - -### 1. **Setup Before Class** - -Extract `quickpoll-demo-complete.zip`: - -```bash -unzip quickpoll-demo-complete.zip -cd quickpoll-demo -npm install -``` - -### 2. **Jump Between Steps During Class** - -Use git checkout to jump to any step: - -```bash -# Go to beginning (step 0) -git checkout stap-0 - -# Progress to step 1 -git checkout stap-1 - -# Jump to step 4 to show the demo form -git checkout stap-4 - -# Go back to the final version -git checkout bonus -``` - -### 3. **Run the Application** - -```bash -npm run dev -``` - -Then open `http://localhost:3000` in your browser and use the projector to show students. - -### 4. **Show the Demo Form (stap-4)** - -When you checkout `stap-4`, a demo page is available at `http://localhost:3000/demo`: - -- Show students how to use the form to test the vote API -- Open DevTools (F12) β†’ Network tab to show the POST request -- Open Console to show the console.log messages -- Display the fetch code example to teach how API calls work - -## What Each Step Covers - -### stap-0: Project Setup -- Create Next.js project with TypeScript and Tailwind -- Set up types and data structures -- Initial folder structure with in-memory "database" -- Includes `.cursorrules` for consistent code style - -**Files to show:** -- `src/types/index.ts` - Poll and VoteBody interfaces -- `src/lib/data.ts` - Sample polls data and functions - -### stap-1: Layout & Navigation -- Add navbar with QuickPoll branding -- Add navigation links (Home, Nieuwe Poll in bonus) -- Update metadata and HTML structure -- Global Tailwind styling - -**Files to show:** -- `src/app/layout.tsx` - Layout component with navbar - -### stap-2: Homepage -- Display all polls as cards -- Show poll question, number of options, total votes -- Add hover effects and links to poll detail pages - -**Files to show:** -- `src/app/page.tsx` - Homepage with poll cards grid -- Tailwind classes for responsive design - -### stap-3: API Routes -- Create `GET /api/polls` - fetch all polls -- Create `GET /api/polls/[id]` - fetch single poll -- Create `POST /api/polls` - create new poll -- Use Next.js 15 async params pattern - -**Files to show:** -- `src/app/api/polls/route.ts` -- `src/app/api/polls/[id]/route.ts` -- Point out `params: Promise<{ id: string }>` pattern - -### stap-4: Vote API Route + Demo Form -- Create `POST /api/polls/[id]/vote` - vote on a poll -- Input validation and error handling -- **Demo form at `/demo`** - interactive testing page -- Console.log statements for teaching -- Display fetch code example - -**Files to show:** -- `src/app/api/polls/[id]/vote/route.ts` -- `src/app/demo/page.tsx` - the demo form (use projector!) -- DevTools Console to show logging -- DevTools Network tab to show POST requests - -### stap-5: Poll Detail Page -- Create `src/app/poll/[id]/page.tsx` (Server Component) -- Add `generateMetadata()` for dynamic SEO -- Use `notFound()` for missing polls -- Placeholder for VoteForm (comes next) - -**Files to show:** -- `src/app/poll/[id]/page.tsx` -- Point out that this is a Server Component (no "use client") - -### stap-6: VoteForm Component -- Create `src/components/VoteForm.tsx` ("use client") -- Implement voting with state management -- Show results after voting with percentage bars -- Purple theme (#7c3aed) with gradient progress bars -- Real-time vote count updates - -**Files to show:** -- `src/components/VoteForm.tsx` -- Explain useState and fetch inside a component -- Show console.log for vote submissions -- Update to `src/app/poll/[id]/page.tsx` to use VoteForm - -### stap-7: Error Handling Pages -- Create `src/app/loading.tsx` - skeleton loader -- Create `src/app/error.tsx` - error boundary -- Create `src/app/not-found.tsx` - global 404 page -- Create `src/app/poll/[id]/not-found.tsx` - poll-specific 404 - -**Files to show:** -- Explain Suspense boundaries and error handling -- Show how Next.js automatically uses these files - -### stap-8: Middleware -- Create `src/middleware.ts` - request logging -- Log request timing and paths -- Use matcher for selective routes - -**Files to show:** -- `src/middleware.ts` -- Point out the matcher configuration - -### bonus: Create Poll Page -- Create `src/app/create/page.tsx` - full form with validation -- Dynamic option inputs (add/remove) -- POST to `/api/polls` and redirect -- Remove demo page (was temporary for stap-4) -- Add "Nieuwe Poll" link to navbar - -**Files to show:** -- `src/app/create/page.tsx` - form implementation -- Error handling and validation - -## Teaching Tips - -### Code Highlighting -- Use VS Code theme with good contrast on projector -- Increase font size (at least 16-18pt) -- Use "Zen Mode" or "Fullscreen" for less distraction - -### Live Coding Strategy -1. **Type slowly** - Let students follow along -2. **Explain as you type** - Narrate what you're doing -3. **Run often** - Show working results frequently -4. **Use DevTools** - Show Network and Console tabs -5. **Ask questions** - Make it interactive - -### DevTools Demonstrations - -**When showing stap-4 demo:** -1. Open http://localhost:3000/demo -2. Open DevTools (F12) -3. Go to Console tab - show console.log messages -4. Go to Network tab -5. Fill the form and click "Stem" -6. Show the POST request in Network tab -7. Show the JSON response -8. Check the Console for detailed logging - -### Key Concepts to Teach - -- **Server Components vs Client Components** - explain when to use "use client" -- **Dynamic Routes** - show how `[id]` creates dynamic pages -- **API Routes** - show how `/api` routes handle requests -- **Middleware** - show how to run code before requests -- **Tailwind CSS** - explain utility-first CSS -- **State Management** - useState in VoteForm -- **Fetch API** - how components communicate with API - -### Projector Tips -- Test the layout on a real projector before class -- Use a light theme for better visibility -- Zoom in on code snippets (Ctrl/Cmd + in browser) -- Keep demos simple - don't try to build everything from scratch - -## File Sizes -- `stap-0.zip` to `stap-8.zip`: ~32-43 KB each -- `bonus.zip`: ~44 KB -- `quickpoll-demo-complete.zip`: ~160 KB (includes full git history) - -## Git Commands for Students - -After you show them the steps, students can explore: - -```bash -# See all commits -git log --oneline - -# See all tags -git tag -l - -# Jump to a specific step -git checkout stap-3 - -# See what changed in this step -git diff stap-2 stap-3 - -# Go back to latest -git checkout bonus -``` - -## Troubleshooting - -### Port 3000 already in use -```bash -npm run dev -- -p 3001 -``` - -### Changes not showing -```bash -# Hard refresh in browser -Ctrl+Shift+R (Windows/Linux) -Cmd+Shift+R (Mac) -``` - -### Git checkout doesn't work -```bash -# Make sure you're in the project directory -cd quickpoll-demo - -# See current status -git status - -# If you have uncommitted changes -git stash - -# Then try checkout again -git checkout stap-5 -``` - -### Build errors -```bash -# Clear Next.js cache -rm -rf .next - -# Reinstall dependencies -npm install - -# Try building -npm run build -``` - -## Project Structure Notes - -- **src/app/** - Next.js App Router pages and layouts -- **src/components/** - Reusable React components -- **src/lib/** - Utility functions and mock data -- **src/types/** - TypeScript interfaces -- **src/middleware.ts** - Request middleware -- **public/** - Static assets - -## Additional Resources - -- Next.js 15 docs: https://nextjs.org/docs -- Tailwind CSS: https://tailwindcss.com/docs -- React: https://react.dev - -## Next Steps After the Demo - -Students can: -1. Modify poll questions and options -2. Add new features (e.g., poll deletion, editing) -3. Connect to a real database (PostgreSQL, MongoDB, etc.) -4. Add authentication -5. Deploy to Vercel - -Good luck with your live-coding demonstration! πŸŽ‰ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/bonus.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/bonus.zip deleted file mode 100644 index 489f7f0..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/bonus.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/quickpoll-demo-complete.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/quickpoll-demo-complete.zip deleted file mode 100644 index 9395caa..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/quickpoll-demo-complete.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-0.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-0.zip deleted file mode 100644 index 69852fd..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-0.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-1.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-1.zip deleted file mode 100644 index f7b3892..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-1.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-2.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-2.zip deleted file mode 100644 index af20edb..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-2.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-3.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-3.zip deleted file mode 100644 index 738da91..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-3.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-4.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-4.zip deleted file mode 100644 index 0ce7241..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-4.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-5.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-5.zip deleted file mode 100644 index 25e5e09..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-5.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-6.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-6.zip deleted file mode 100644 index c46f5b6..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-6.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-7.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-7.zip deleted file mode 100644 index b9edaa0..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-7.zip and /dev/null differ diff --git a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-8.zip b/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-8.zip deleted file mode 100644 index 0ccdc0a..0000000 Binary files a/Les06-NextJS-QuickPoll-Part2 2/demo-zips/stap-8.zip and /dev/null differ diff --git a/Les07-Nextjs/Les07-Docenttekst.md b/Les07-Nextjs/Les07-Docenttekst.md deleted file mode 100644 index bcc7ba4..0000000 --- a/Les07-Nextjs/Les07-Docenttekst.md +++ /dev/null @@ -1,179 +0,0 @@ -# Les 7 β€” Docenttekst -## Van In-Memory naar Supabase - ---- - -## Lesoverzicht - -| Gegeven | Details | -|---------|---------| -| **Les** | 7 van 18 | -| **Onderwerp** | Supabase: database koppelen aan Next.js | -| **Duur** | 3 uur (09:00 – 12:00) | -| **Voorbereiding** | Werkend QuickPoll project uit Les 6, Supabase account | -| **Benodigdheden** | Laptop, Cursor/VS Code, browser, GitHub account | - ---- - -## Leerdoelen - -Na deze les kunnen studenten: -1. Uitleggen wat Supabase is en waarvoor je het gebruikt -2. Via de Supabase GUI tabellen aanmaken met relaties (foreign keys) -3. RLS policies instellen voor publieke toegang -4. De Supabase JavaScript client gebruiken in een Next.js project -5. Data ophalen en muteren via de Supabase client (select, update, insert) -6. Environment variables gebruiken voor API keys - ---- - -## Tijdsplanning - -### 09:00 – 09:10 | Welkom & Terugblik (10 min) - -- Vraag: "Wie heeft het huiswerk gemaakt? Waar liep je tegenaan?" -- Kort terugblikken: wat hebben we gebouwd in Les 6? -- Aankondigen: "Vandaag maken we de app echt werkend Γ©n koppelen we een database" - -### 09:10 – 09:30 | DEEL 1: Poll afmaken (20 min) - -**Stap 1.1** β€” `votePoll()` in data.ts (5 min) -- Leg uit: we missen een functie om stemmen te verwerken -- Typ de functie, leg de logica uit - -**Stap 1.2** β€” POST route fixen (5 min) -- Laat zien dat de huidige route alleen logt -- Vervang met werkende versie: body uitlezen, votePoll aanroepen, response sturen - -**Stap 1.3** β€” Server Component + VoteForm split (10 min) -- DIT IS HET KERNMOMENT: leg het patroon uit -- Maak `components/VoteForm.tsx` β€” Client Component met useState + fetch -- Herschrijf `app/poll/[id]/page.tsx` als Server Component (geen 'use client'!) -- Server Component haalt data β†’ geeft als prop door β†’ Client Component doet interactie -- Dit patroon verandert NIET meer als we Supabase koppelen - -**Stap 1.4** β€” GET route + visuele feedback (5 min) -- Voeg GET route toe (nodig voor VoteForm na het stemmen) -- Update PollItem met percentage bars - -**βœ… Check:** Demo op localhost β€” stem, zie de bar groeien - -> **Tip:** Als studenten vastlopen, laat ze de browser console openen. Fouten in fetch calls verschijnen daar. - -### 09:30 – 10:15 | DEEL 2: Supabase Introductie β€” No Code (45 min) - -Dit is voor veel studenten hun EERSTE database-ervaring. Neem de tijd! - -**Stap 2.1** β€” Wat is Supabase? (5 min) -- Open-source Firebase alternatief -- PostgreSQL = echte database, 30+ jaar oud -- Gratis tier voor leren - -**Stap 2.2** β€” Project aanmaken (5 min) -- Iedereen logt in op supabase.com (GitHub) -- Maak samen een project aan -- Wacht tot het klaar is (~30 sec) - -> **Tip:** Zorg dat iedereen een GitHub account heeft! Dit is een veelvoorkomend blokpunt. - -**Stap 2.3** β€” polls tabel (10 min) -- Table Editor β†’ New Table -- Leg uit: id (uuid), created_at (timestamp), question (text) -- Leg uit: NOT NULL β€” het veld mag niet leeg zijn - -**Stap 2.4** β€” options tabel + foreign key (10 min) -- Vier kolommen: poll_id, text, votes -- Foreign key: leg uit wat dat is en waarom -- CASCADE: als poll weg is, opties ook - -**Stap 2.5** β€” RLS policies (5 min) -- Leg uit: standaard mag niemand iets β†’ we geven toestemming -- Maak SELECT policy op polls, SELECT + UPDATE op options - -**Stap 2.6** β€” Testdata invoeren (5 min) -- Voeg 2 polls + 8 opties toe via de GUI - -**Stap 2.7** β€” SQL Editor (5 min) -- Laat SELECT, WHERE en JOIN zien -- Dit is een mooi "aha-moment" β€” de data die ze net hebben ingevoerd komt terug - -> **Tip:** Laat studenten ook zelf een query proberen. Bijv: "Hoeveel opties heeft poll X?" - -### 10:15 – 10:30 | PAUZE (15 min) - -### 10:30 – 11:30 | DEEL 3: Supabase koppelen aan Next.js (60 min) - -**Stap 3.1** β€” npm install (2 min) -- `npm install @supabase/supabase-js` - -**Stap 3.2** β€” .env.local (5 min) -- Kopieer URL + anon key uit Supabase dashboard -- Leg uit: NEXT_PUBLIC_ prefix, .gitignore -- ⚠️ Check: staat .env.local in .gitignore? - -**Stap 3.3** β€” supabase.ts client (3 min) -- Drie regels code, simpel - -**Stap 3.4** β€” Types updaten (5 min) -- Poll + Option interfaces -- Leg uit: waarom twee types? Matcht de twee tabellen - -**Stap 3.5** β€” data.ts herschrijven (15 min) -- Dit is het kernstuk. Neem de tijd. -- `getPolls()`: select met options(*) -- `getPollById()`: select met eq + single -- `votePoll()`: twee stappen (fetch β†’ update) -- Leg de Supabase syntax stap voor stap uit - -> **Tip:** Als studenten TypeScript errors krijgen, check of ze de types correct hebben geΓΌpdatet. - -**Stap 3.6** β€” Homepage async maken (5 min) -- `async function Home()` + `await getPolls()` -- Leg uit: Server Component mag async zijn - -**Stap 3.7** β€” PollItem aanpassen (5 min) -- option.text, option.votes, option.id - -**Stap 3.8** β€” VoteForm + detail pagina + API routes (10 min) -- VoteForm: optionId (uuid) i.p.v. optionIndex (number) -- Page.tsx: alleen `await` toevoegen β€” structuur verandert niet! -- Benadruk: "DΓ‘t is de kracht van het Server Component + Client Component patroon" - -**Stap 3.9** β€” Testen! (10 min) -- Samen testen op localhost -- Stem, refresh, check Supabase Table Editor -- Het "aha-moment": data overleeft een server restart! - -> **Tip:** Open het Supabase dashboard naast de app. Stem in de app, refresh de Table Editor β€” studenten zien het live veranderen. - -### 11:30 – 11:45 | Vragen & Reflectie (15 min) - -Mogelijke vragen: -- "Kan iedereen nu stemmen?" β†’ Ja, via de anon key + RLS policies -- "Is dit veilig?" β†’ Voor een leerproject ja. In productie: auth + strakkere RLS -- "Hoe voorkom je dubbel stemmen?" β†’ Met authenticatie (volgende les!) -- "Wat als de database offline is?" β†’ Error handling in onze functies - -### 11:45 – 12:00 | Huiswerk & Afsluiting (15 min) - -**Opdracht:** -1. Maak een /create pagina (formulier β†’ INSERT in Supabase) -2. Voeg navbar link toe -3. Extra: SQL queries schrijven - -**Vooruitblik:** "Volgende les: Supabase Auth β€” inloggen, registreren, en bijhouden wie er stemt." - ---- - -## Veelvoorkomende problemen - -| Probleem | Oplossing | -|----------|-----------| -| "supabase.com laadt niet" | Probeer een andere browser of incognito | -| "Kan geen project aanmaken" | Check of ze een org hebben, anders aanmaken | -| Foreign key lukt niet | Check of de polls tabel al bestaat en data heeft | -| RLS blokkeert alles | Check of de policies correct zijn (anon, SELECT, true) | -| .env.local werkt niet | Server herstarten na aanmaken .env.local! | -| TypeScript errors na type change | Check imports, rebuild, herstart TS server | -| "Data verschijnt niet" | Check browser console voor Supabase errors | -| Votes updaten niet | Check of UPDATE RLS policy op options staat | diff --git a/Les07-Nextjs/Les07-Lesopdracht.pdf b/Les07-Nextjs/Les07-Lesopdracht.pdf deleted file mode 100644 index 3463197..0000000 --- a/Les07-Nextjs/Les07-Lesopdracht.pdf +++ /dev/null @@ -1,334 +0,0 @@ -%PDF-1.4 -%“Œ‹ž ReportLab Generated PDF document (opensource) -1 0 obj -<< -/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 5 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 -<< -/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font ->> -endobj -5 0 obj -<< -/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font ->> -endobj -6 0 obj -<< -/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -7 0 obj -<< -/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -8 0 obj -<< -/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -9 0 obj -<< -/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -10 0 obj -<< -/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -11 0 obj -<< -/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -12 0 obj -<< -/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -13 0 obj -<< -/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -14 0 obj -<< -/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -15 0 obj -<< -/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -16 0 obj -<< -/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -17 0 obj -<< -/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -18 0 obj -<< -/Contents 35 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -19 0 obj -<< -/Contents 36 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 22 0 R /Resources << -/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] ->> /Rotate 0 /Trans << - ->> - /Type /Page ->> -endobj -20 0 obj -<< -/PageMode /UseNone /Pages 22 0 R /Type /Catalog ->> -endobj -21 0 obj -<< -/Author (\(anonymous\)) /CreationDate (D:20260324182754+01'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260324182754+01'00') /Producer (ReportLab PDF Library - \(opensource\)) - /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False ->> -endobj -22 0 obj -<< -/Count 14 /Kids [ 6 0 R 7 0 R 8 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 19 0 R ] /Type /Pages ->> -endobj -23 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 779 ->> -stream -Gatn$968f@&BF8='RNM7K4;NjgtG=AFOHsA*dZf)Jsp/aFpp4l\*qHm0S<;9$8r0rX53eO=tLFT]P.+$>lhTri9CCJarpNe<"K)X/\I-rVh!?iM@jEf`@jgr6Rbb<7Ruj/2tj/".&cRn0u+d99fp-aX"("qF#'AME^aeX/NQ9U\@B-m&)gI\kgn3i=giti@iF26iRYPK3D\'rL_>O#^n6e-R'[oAMi?mWO!O:FpNkhTNL4M%ML/g(!'<+u!F6hDMGEP&-I`^#WJg9`L//<"B:biKZ<>5q`6cia]R'E2i/Vl=%Qs/u=:19M"^:70Dr.-\b$[C=dnW.q^>)_m6="kKZO:\iB"08SE5R[\DI:D#/7?fXR9&IRV;C*m"4EjGb6B!=LK0/h5+itY^4Pt!DB5Tqb?l!NOb!Q5G$):T&=;\1=?_ud!*fijm+1(B4.PT@NqMJutU;uS(>:rKc84kH`Of!u:iA[nF2>cA4934Is01^AsKPDddoLKFd@#KOsJfa,sPVn#T9Y'>+YIVt**IWL+D@FePP):%WTph9"UV0XZ8n2CX8en-3b3Hj,W;,g$/LS3im$!Y,JQWBQ:KM-m)'hE4u1aoF'c%ACQ(O_^e\c~>endstream -endobj -24 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1029 ->> -stream -Gau1/gN&c;&;KZP'RT)O'7qJ>VDk%eEpubkPF;%i2_rH4%Nls+Vg8/3**fD$-Z[6_*gcu(rE._P?U3PL"Z>VA!;IJMaVb/5+eA`**"V%%3/n/="4WkZ?mgbLKHTnS#=UqS`i6ic%WLY-i/j'o+q.47oFJlfT]SXH@H'V-^jqmcH@6+;EOk#.[-#o/Y1XE*98GgrYd\@m.l8E0Mb0R/^O6/%D,,>%%H(q#jQj#g"!NZ/Ri[=A)9W&/C8dTVZm1[5/fstteKa",kP6j7.\O,"5-jbX.gi"i(U1X`XMVSDGd%!'nXB:2hFo2s!'(<+0FhO9-.?fD_hTJ(Xia%D#3cW7^a^=AiGPeG0aPLV%%`k`BhQ9!HbOD5OL0NO-Itl+2dTIrA>Os?E3Bn#`^O=O83Er3?H[SBg?2:Q[cVCDPWqR;ZM?*,I9*ORc="Xo3i0sYqd=l+%pR)!Y1N'gG"4=pJE$fS0b%glM[?jTm=/hk7ibok%4qU3-(BR[7^AUC&Q'#ocFlRhNEEqR[9f@cn8e8MRl!@N=\30c,$8,YXUf;/PCb,#KN(?*)JJZ%l&]4ntDem:oFPNDtPQErXb!0R>5p3n>NYUK9OlMK:en/8!P5u)@"%>E0`W~>endstream -endobj -25 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1346 ->> -stream -Gau0C>B^uE&:WeD9`9VX/67+))KL.Eh"KeK[rH4Zq[i>&Ad?tUPr=!Z,rY_h*D$WF*+MO.)N6r0b^B#AoP):\U)2FRP76Bo#l;;Ui&6Ok^g6*>kQH>TFZt"o1Xih$+5%6]^^D!?r.s]QR+]1o\/m=O[%,2A(-1XjMHP^^nlD,T7NlTPFU7Yc!o7A)"n2P_b/N?gc?"f>=aZ(c&4W:"oKj^G:TpZm3-H5E\*+'Pa6d@NY!G@1Eh0/,;>cEHUV\!$-nELsqOS%UiX"kq1TosXHEOHCQIc.VqI8-1Sl[c_9fF=Ti=V?]bs!ocY>I.NW>[_'nAFM4L/r`]jW82Y6ks[gnX-cmu1UN%p"e)UBU'`2K`3E-a%X+c6"WQNXO]o*\\39`&_`YHsYfO;*d"TLl'h?&:W8;FUZ&m1[>6,EDDdMYC)ru^aGhb%?3=tR%%rZJ[lcQFY7B;;.E2RDI]Q=V%X(EPE=[B4Q%s:f+Z:J[)FE:VO6Sn$cj=F#53#k8RN":Epjt3.@\GMJ16ad/!FWhek4E&cm"C;[Kp\4(9i3etf0JYF^Z3?+g2?/tEg0AW#IV?:lq"!NIMoN.[Cs?,NZ-GK#LPaJ7Y4p6YM3k#!BSn-XW^mXIF.?(1S>q->1H*->P0P1%HYSbhV4;6MSU40"*L#.0eK2$Xg&+@`Q>L0uo\KZ;1*i,T\uGP9N#OrY@r.,O69p.F#4Fh8,_.;k7?bdhY>R_R(F[57l>t0.D6!%U'f_D6sq[[D=NpQ+4\U9Fmm0mhCrldsb>QV8NE/TFp*IoaEk.N54T&44Kj"cO>JYt?$H\*OH7bRg9kH_SVkRYmJE=3tN$TP?]p`aT6WX7JSaF.[rDQrBQJNhm\=r,kq*%R9g&("RB3M&lR)0eot]T?O'q\F/-J^2#Jgh*\~>endstream -endobj -26 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1616 ->> -stream -Gau0CD/\/e&H;*)EAIeukn;.%-!Br-@I&8p[`K;[?`F0=/372`OfCt8"bH^NNg-B]Na995Q37dN+7C9-4ac\]5nAH.qgXb2]GQ:u`WOPBnJ&Wd">G98YK*2P*H,@(q'6mT6OsB`8%`7e`-(i4nTsuHK@OU[hEdc?'*]K?(r?M2Z6"Y_cOI+nnTQ!5^Y]+%KYSNF#[V0GgV%n;aCbA.]]'j>Jd\I7N+GtYQi-DHBa+akV3D%kJ<0%tP=mcbP_"O$pLmho"[2%#0&G.[*TN;;nT_MlB96"c5o(9$At%6(FoKT?Zbc_$6F2hM1hYDKPb)/HiZP$ZB-Q#])d0&lHD^ouocljX1N"Bo0itWBm2:q=K8gbW#nEs#WEbZN-^+9JCQ$Q=hm=>rdm*T0eNPm*^\!,`H)uCRI!dY;phdHd>-\h9K\dn"r"VbB6I3c=17fBGDha!LD-(e0Sa?kF6NV^lFVYIm]VW?ZJcBnU+n!4rG&GtL;lP&X_[fs+i.?ZestQ"oBApkq2kJrE>%FU$?SL!C3%R5nODl9VbK'4ra*A:e[>n?f8-hQ8gJJakdB/FhXY*nB_>=G@H0ZObEg!_@ojgEK4E:/+_XWUK[!8\iCBP]1(le0WAA-_Tg9aBcX2_EsK`3;b1Y7+l?,GSLN$OMo>GVdXo(XOE,ib[fLb8s15rqm,^a_oS#t/cojBQ*I%7Mio5R;Pa0CU\,0't(^YUE7OiT/.kOSn-e)ti]i7mO@K*&>B/7.>_-a+\+%]Hc5.ra/CM+^q[A33dcDBGtA4g,6,-MDfCHa(?0i>Pa*,q1I?U_Lqhj!HE#7lC*j+hS[jtp;kl9fIIT?dMEFCq)@0u7Kk33ZuHQZPVpM.V#8\[DQqS2ZL&8fqcd/---DT)1[Uu)*`1:CL=-Wc&D7%^SV=?>q2Y!,A6!ZPe_:W(%dregLI`Cic]X8dKtO>XNO0a$aj,/E.X`CSocg4\RO+4"^1;FVmVsGkeE7>X">T.;``+o6CXI!@a\jbfnNs'RkE.Igtk:)-;6h0TTtRt8&&5;Ll*]d/&7K1:&,E?d?s:"?>H6QjOr2pk0b1\L)Z1Un%I][e0*miW[:fM06bf2al`kp]mb9~>endstream -endobj -27 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 837 ->> -stream -Gat$u>u03/'RfGR39sM#^kq[5U^1[$!eeKM7O.W2m(mPo&npZo\-7W`he0QtZtomtET4q1\bFn>lV$4$S(dUU$63\t:FW6'C]kZ>&D74^ZIXG^Y#`P+M,QX8kf+d>Bq/q.T%60<[\Zi"d)]Us2/93)lKRF&YJ)'ql-Ch;?k(/IX=?EmJom+1,24of^[(c(rW@)Vs)./We9jX.4nV*N)e*8I3dnss:`XE@qd#__A>nT`E@ArlP)m0KR#ZXaP(TY3X2ba2)a5*X!Nr%r5!g%H(r'Xu'FM"4@c[_-)jc/EJP&Za\75TG\m\&t>\j0_%$^&pD"O$F_lCjj<+B"EZ?;6j"$+tL[%#6Wr@IF04r,H%ilsVYd_lMk]^;oJ=sUsPMkmW09gZM[oc=G[,c68e6kTsX\J_1)38V!%\)m=_eN6mu*?b;OmW)LQ>2^?/XtI*ra%tm;53>a_o7mK=[@Euifq"?0=)6.9LGj;h]QkLrBFV3.8IR>BM4lDqh7)1=q>H72We[Y:a1c^qZ'nB+/G,T4qpPK+*B^O-3j>50h,'!Kn1+l@jp:=)Q"jmln9p8?Lni51Ei1JZc;%J%dQ@EL^tpuD5>7DBCNKuFUr>iUaWe%>inou>dUai@PitA=job%KikuTCp@bb\+aGJ)\s6.;Kc(.Ck>KE,cb[X$c0,cFiE*V^mG:jB>n>WL]+;h0L;Vaej[S~>endstream -endobj -28 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1134 ->> -stream -Gatm:>E@K-'Ro4HS>''S7bDs5_^;4^'"(T4Zq7m.:HW0l&)^`?m*(FA-&DTXisVqS]A<+2&P(L:npTDScPZpF?**j]F+Dk`)#qI'3A=e@o.1F7n)FN"05GC=e+FPA2sfKb"6%cQQsZ1tfWlF0s(Hj\ja0k@LWW1.Q0_\^pYX:CceAK67&W]dp/Z850qKC3.:23.(%e90'sa!#:/Qi^kr2?&peM2Bl+!?@K#jch[1e@S4N\niUtBknG1N.XO5:0F`S^P:::t(6AVt41j[Q2W`^2gaX/n3TiMU4Y`_Eb_k4qp=/$V)r#4Jh*ZW`/;!PD!+(\;!R)/bFA%OQf;`&Aj;H9T'rJIE%W2:];I#7DoD^1h@4demSajTtXa!iPlZ7`:ff)#;0q9u.+ic*0Cmi/]T+dB]K66m>d*nni5(?_WODZh'\31T\qnVA$10FQni1>fPf8[laIZi.c)`uaJK8%+>V_c,769q64"IcEH6L(Z;Fdk*o#1E@l<(RBbK:kC5JO^J61,ul,5Z:K\HgREe$Np1S$XsMtn^-1i0HsP\PW?)8X<(0i%"C77h%2l<*R>nbWBZ!]HAQGMHC`BbB6DZP>P@rS9gNVf2;?.sUjW'-%K9rZ=+jFBrI&%l]d%l"Xi`;;WOSIjWmEj]'>j5DudbpV>b3COGC?mu-k\BtW'<%<0#`N.(;$*9oJZYNt355N4^.P0D7jrLIR+j-afP4-*/])RF9RFkW.-*%dHs#0t-l.rZ_g#,UELF]3.'lC/7&[riYLe5\o)K<,4#Pn)LRef4hU&nk?38\JT_9UBq,hB72;BpL+5$jA93*$pJ\/ga>;$>B8tZ`UGn"OLkUrLOOB5Nm@'Sn0^ME"aPX4Xuh@o:-TnSM5Q4]TB`@h/u`g-!/_Lgs=%Cb7;2,H,]j(F=:V=kO\BXUmMq[2DZ)T?4n/Nso.ASgkZJBZSI>@W@%H^sE'8`f?`/`dX8~>endstream -endobj -29 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1711 ->> -stream -Gau0C8U&oI&AII3bY^qPL&cF)qGP@Wdl=DYjPoq)+:4?J;1LubG25N8HNtZa;%aboWU!95B#-F\2\Z9/^PVbJ>64X[JX:!P#_gAX&E!"dEWN)%dJ"6XRNZgjKI-hr+EC!c->80DLP@YV")tp*ob%U,ior,;(F/QD>1ZGKGQe7cY0_YO@/QJb%-TFY&Ls.=Ei)_EQfXZ1=i''SdKn]&30A:]$mrG;H*/<=_XB*Ze8)rG/fk;b^@&k42(TB/aC>><;R&)ULK"i!1'MC>i;.Qr(>&Kk/:N9VX$@)ZTYaU)"!MV95d%YB;91MbEB$U4DIb:s4;SFTGO<37>er8YF*2jIBb2XDUg%dNK;V%E11X5S#6X"IZ;?IL?[fS:3E$-i6*Q2[>\/#JbA1DuW>%J4K"(C2C@&LG^b''leMC^:nSbQP^FIbC(Fl!?@(X*bEfl5(aS@#3<3GnSi.JI*kN>_q1-D2,lM#>QIDr%G3Eu,8TCP<'0ulVFlV')3Vp!%lNYP,eI/q_XNoh070+Ig^F;lc+Z_Eig+/-(B=ckS_C0I/%lhROA36Zc@+9`8\U[Yp$>4\:8(GL_G+JeS]5E&Cn)7dVFhRrbapbI8Y-ftC'gLRHo=[h1bI,R"h0>"=1JXJTq^,ZiDW(._*5OFIoAt+_.m&/@+9KbD(YF]!Umg`jAluC6\h2r@-l^K2H@>$ZPs)JGV5$(9T&Y=%X\&;j&puPgu>!4.B^]]FSs_Dg#"BBC%sWSH+oWg$(E,IT?^B2aSjh:G.LC41BgV/Ib1fR9dPW`#n;G4nZYLnU%_Rn?4o;=i;2qpqR?dgqYA3(68sdMfR:.s&T&EETe\rj:`%K3jI1s'qf('-E"'!s_AU+RRAGHaTU$(]"4]YC+nJ=X22X7Fnj?eNBRHWI"/r;-=nZPfW3#.%4C,5i!LEWaa-r8cg54[mN57;U5G"9TR"&W2!#Vc?G:;n6eQVp"!0]9bc*(9`K1dG.g[DaYDn?t%Q:A>&m-g5?:US!!gB'Lf!Z[u$!)Jl1&?j6<^8X+L2c1JoH-tX6@)\c7=H&"6;mWK2Z)V&uVl*&V.eUoIn`i3UQ=d=WPaD."@bE&%MtHO5R2D`>0Sh07n,EMj''cp,o]bmV8SC5R/Sj/CFBP[D-b^k/5E%:3`;tp#X$27n/b]dQ9WLCJ19JAhb-\7R+&0I4rA$Bs3DUI.AB)'k^K%0$'b/%)5dVifj-#VOO5T*LjiORTV[(dL5F([hTYmRD2*4pTA)l?9[FLZClR!>r*jfgRqRJi(E&806pg"sHXdkA[pWdJ7dR;Iud>bKhIJ5rZWXZW/9i]B;8(BBk_?@L`0Ac5a6p`T"/F*hYh.7*fkd[<.D0q6"-/r`DoPsl-h@2;+E4itVfa7d6G(LQ>2=KeF@Iu2o)Tot~>endstream -endobj -30 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1410 ->> -stream -Gatm;>Bed\&:WeDbW_teI%TX2U@oM-tVS@RM2e,1)1oK,FBtf_l)inS8$4aeLqAi?45)+$$8qA`'Dh5L=k%FSb)$WoZfk6?SD?@[>M7R^La70`MVcnTG9R#0[%r;h^!r1.$huNX<^`#Dge@KZKB1/Ft'gbbD?jice!<4Ff?8`Aj8/1)>ttW^N)5k7>KTcm)WeIa0I.2nZGeo")`+oTs%F.ED>6F6ug>c%$B9f8TB13#G6!PHcqkai[5OWQ=d;KYO^paOM&/kNQX4*p-*4LkSB.,f]F"W?1"fbb?Mf@$q:f#`,6*Jn]<3WV@dF_fC-3,a3r'#b4E4"1n)jW*(*W2*M9OUW`f]peBM2a*K?Cde0/K^jlPnq3&1!W94!(%6WWka+Y"5:^FQoBT^)OHG0b_Y=?Y*)G3!98k9o&:ZQ!lPRj-A$k5^PGRUCGg0o(JdKZd/9GA(ni:C@=;*q6X/UEe&0r%Bg`2>K\qrmD$k6B@30M@i%U2SVQ?ki+#I:#'EAqLec=\;.N/7Tl>Mk&#WK1#Lt^N!)X:jFPX`IFJ]#2lR[s(K!!@rR;6p(H)4OR/*5i(4!]%*;9-rnm=r2Iu]df&mt/jBb<4PqI0_\%][B4+]ci)=(bgU,S'P4+u7$W[!0(*%D_Dq-n@?'\A2JUkTg_:Mq,YL42m8Q>8:9(sWC+4i(.\pp3`S\)2+#C.h)6V`"Ju,rT.sm+@2$i5)(>!I2r12.HJ]P?]=-Y!RM;OPBK(?d%'6jYROp0mLrS641Q1(8>U8OX%k\=M(^VC10pg[%:AWbk`2dnB/F"RgUcq*?$9\S>Be2Cb"Dq_QG)slnn3kkO%4srOo@H4C,JWC%Nj7T'S<;!\*h:m*qS_VcLFuk/Qe4*J&]8pd_VBSE0!]Y_ZiPi>3]s"7>JI>`H0O.#6C1#/P(aleLGgNL/>KhiU5@=9)IbdGBJ2`[>^?ip\#=EQR\$$T\hiR4N@jg)cj$PBZGSQoIg\Jq%(uB0@ZRB;^/WGguaM7e9c)'9uc/+K$9#4L_f6U(<"r7Lb>oc0MB5eY]mIFLd=8.IaK9\;t7PF#%T"l[O))9^;~>endstream -endobj -31 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1255 ->> -stream -Gau0CCNJ2h'`H=\\EtZtfa,c>PKD%32OhgC->=Tnh:_-K[S_j4O^)%NA39bY/&/kZYddtj);CgbHdBAd7h6#s\o;g'B)ot>kkp/9lS,sRCa'OII5i+Q^:%uhC$Wk6F:X"63='u?Vu!B)Ut`Ij>(@#_1HZ\>B;?U\PEtcr.q+/g'g`-g$^'kp7TkB3DhaUk//J;"!7Bf,Zh\M=dRaQ]s=WU$5PoY*YFWlHeeF(:)l'KfiR\>SHBu"d?cRRsn)`6E]SX\d*i&nbXkTe,7EP56JPL4X3U\`A=04;XeI"PeC#0*&OY'D7f\J[[pRtKIs.\CdseFKeKAH7&1,3e#ECo=T`K:Yqgdg2"l+6cp%,Rq7_A1C'HW_dN/rK.@VSoK3:%l^>$-.A_4L-/^V*j.pC*jD)DAfas/n/0dT6'R)$sh+&_YccA?BH''TsF09h&eo+s(f;UV##MP92mI:#=%%]&I#)>(',!)1o9CT`RG0!!HTro53fCP1n-q]MHq?M4jbN;D-PWiBDHn+:*\QQ>OY,X/+bA)"7RW-V8s:DiP&-$0VW'Nb5;aNG7ItT>ncjP8.DMj5,#;?Jl[G,:C7;X!$Ktj^d;Rn_2AfpugUABN>S4m:Qm=AH`sOHR%E*4U^$4K2]3%/n_"8D(>YY!0;fKA%_57%#=[c4:4dEOB*K[UO)[V"-gl>tCbf1[O'YPHT%j8BTU\_jmUaOY:pIni6<0FQ#!%qk7~>endstream -endobj -32 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1242 ->> -stream -Gb!;c>BAd^'Ro4HSFU4=XK[)iOLEW[9Y4j_S\9^0Qh=*DMZrXj87#N)@1rQdO(92'&d"c>'Gpt9cLj7hr^=TB2`9LQ)8P>>\pmVN%ZD`"4lX)poMH*b$+9:6Ku[[eD4s_92M6m"BYjcg(_`T$?sP:KJP1BH*2a#r;iIXVDKoKHDt;uDJdY[ORD\\[@p`BT4#$L8UGM;j#CJc!SJsK"gN(-Z/l5mo2R-0IaDV$oqS_t/'2s]I\s.5+`/U;-J9,*<1#Ws$-FuoQ88VgRt`.e3)HmH$ljpLOMW8N1@9QE.G2X/dkm0aT1Nd%i0]hHmrGaGBtMRC[s?+%n$d1@uur6>SQ@O];NR>\<^RW>6i'Tj,Q#4NZK,ZQ^(^Qa@WrqrVjr>XO1]I:%bLTqPIG"I2YYcG_E$aTNg2`#j+u'A'/rkS>[-Dedj9cf2f^S.^#J)/1j"iQU.1GiSMGM:7?t7jL(!r&Qt`,s/e=3L_8GPniB#lZU(mW9Q*!(WLld#`2\[8G(O'.E1Y&Fe2UGIC]ZarY-]9RpBL,]c8[QYi+IF,Xc@(O9d3rtO0?Y&M0l$"i^oWP9Gos=&C5>+i&U[pU^!6<)obeBfX[/$V@o\=RN5+Jc;T[QirdD:5QG@"MhL?FM9L6#'l_?_jFG&4hPoaN:t!P%jG[_fb(2:0Z(g/c^Ge"A:.?VgAc*usA6IL6:*FVi4+]JI-<[$lHKIcchCdq-3l_lRR`73U@*P3c'$$^*Zc<<1SRs-\?/.%^Jef7h!SR=0qY[qqW'*$X$n*6qjnTMh5S`G-;WHCLfAdE=c`'S/n$)te0\UH#K+_eq:RBH5H&>4onZ?@T!(GbY-@SFA\ET1Hcf0tq4#]@^'):)ZhLDq7S8Ms'sXm'gi3H,)>JG4o0`.c4dX$.$X3Q3jt"oC~>endstream -endobj -33 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 675 ->> -stream -Gatm8?#uH"'Rf.GSB:C(drAfI7@;XiUoBp/fT0"/:HXKp%eMZf.6=))mBY5Dn,G&=c*X$c1.RKXZ(@`HN.G;4`8t;V=3;[`=:O_7]0*ebeg8E"_P0m"r-.Tmor6'(]7hjEk%LIbJ/;HDggH)U1W4eB=2_Kl.!K;#_?U-\ps,C5SuqFfUNR*j%0=jLG)+A!@A@X6oc.q??IQK?73[F&g;g3m3u;)TI9m\bTCNJR8Bq[_gVlH%h`JoNs`.0PnFJgoAcdM0+&L8`j-UgtpHDSt]s_M-<]NI""CYqPD,TTnYue&a)uKRZc()RT_ks=)-A=D?+gl50dA%EH.AdE#BIZ7LHJD'G["V!`CKVA9K=V]Uu%lR?FSChX"Y<`k*]ji$S%6lQQk?DmGh&abAK>NU8E`MiK4361;u:*JBa/_0=mXV:qPP3m1Q#;3YLnQq'B8>jaH*7HBu,jD+RJ1m^L'\l6HsfBPendstream -endobj -34 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1715 ->> -stream -Gau0D>BAd^'Ro4HS2/i%-O!qJRpjKXRr,W%B.msdpPFYB>C[A3GqGG;1B-`*Oq"Z>,\5%%L^%'EQga-cqMYS)^+B6?>6%oeT^fi"bD+CYX;5NdkTg!hWdLjLq94jn3^()f6+:>9X4QUp3kS0<'>IYR43"T6g9e;+d-q*BfO/j/$V*32Y,!"&PB:5LgKh>M@5sKn!NsTi&E@lk@Q0`LZ8QuT?)4&"mo9EW`L#Y?UUD]-:MDX+d(bJT=-Ij!+3Nn8snZ%9-aVb5cu:IC`L%IEh*D7X-b`HR+ml@%I-\?pd#)LB:SWq:>#SM19o=*jSff]F]GKZb)l_C.pur#"o;]>`0'>_Qh2mMF@h1Ukb,nBPC1^QO:bj(BGTlnO8X2d4L%mmEc3`rTE0(@u8&(e#9%n0$=]S87s`X.g\i\h814kq;L#1HS#6KK5alt%S.l:s`".p=[Y=cJ96pG+erE9r4&Yc,SQH22M0GKuX]\f,CR2"8uleCe)!$`Do]:("+>/::S((_k1";4qf+"JB^mk4c%u=#.`hB-$g)*9qOEEJghIDK-fKc=Fi=8.MWVWN==M-D&'dJaCqb"fo>XK&Y$.ET<']$Cp1+*.-P1>f[nGmPQ`iM9<=467ofr7hKkaV?<8a>O("?*ENpGY#Sf8*hR_`'!l%V!oJlA_\"e'^)]#7^$`O!^#XkbXo4TePT3j7,lKLX+SM+BrSn8S4I>SA;d(i05ShN9W0T(@4)tk*hC*`OMV[P;P`(>Iii5ZRIQR)0p7PlJq-?8T`7L6;jI[(n9@mb$>r)bG<.A,TAe2/Sg8*D>iOoE%Rgs-o(t\Ib#Zhb>r!/'jYG)~>endstream -endobj -35 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 736 ->> -stream -Gau`P?#SFN'Re<23:0XJ;%r+S9RC1t'iNZV,MH(&;IUbSEMsmk/&ZqpFiYDr/(1e[J\fruch6gNcJ`a+bFAOZ%-XVQ%QLtVF:@Rn+6b)s;sVQbmE3de@H&UR5nYoDKF-W$0oM7JnIk3_[hfefn8,?TFJ@F.I]k)a`*$oF*sX>D(c_JY5l]?FU!,o]DcuYP6TP>D%'T*76Xkj,mLU'2Y#c:hialGq)S(<^,E*FZ<0rlg`Hdp*8j"\YY'[\7"YA)dA*>Y&K(>6fbi,Xe#4u-N?V*r!n&m+(h^(krhGm/QAQ-AS0KMn*#iV9De-NCB7Koj>WRq(_EY8=bThsms8Yh@(m2[";SlYY8_s'5qT]i0%3KqPl-(2>9tLL)ellVMTA9'@]Zf8[u]1Q,/k?9ID].ZX3^[k?0DRl`0=9o.SR'/Tfi3%AsI[rU`FHaR-44S(-cdqaLDf1$>]YLFX1B_%.Js^e"k8nVU_>4K!npIM~>endstream -endobj -36 0 obj -<< -/Filter [ /ASCII85Decode /FlateDecode ] /Length 1081 ->> -stream -GatU2hf#8L&:Vr41"RfHe5FMO&GgFq8\*YLmG;`5G$c`$S1k*o?tTqEHhZ.LfT:cnM43]r!l9,qD`mX9?j7YmbaoUc^n3&\s2BNaO?N[Qh%8!rR&6XN,M(0\qsHH6'*`Fj].=gIA4M\dJ*b3a"E#p`K#@mQ$-WWr4sEZYTLgZ.nA2RJk'REZVcMseD"@Jia<0igLUZ8.JY(qdMqAG*F5ChsnGZ)iGY1X-_)Es(#%NP%PZo3=frF4EILI+t(1M[%HH0X;rYS1HEhQC/WQdgh#J>g$g@D^RVcchRnZ-1k!A0M_*+.t`9O'fOY08a36,"mR$ge#^RRY`<(n?qOHdDA;@FQ5OM'dZ>Vg2!=\F)#$@SMY!"J^5!n;$`;L7_"p4^>^TJ;GBV=]]RZ8Hlh:D/mCX(VTpb]cip!(k4Y;(7Q?+UA7S,P8b/Jt)*R3?bZfshl:tDWt.2jmUf6kkZKLa&ke^3eGQB*-EO4XX,"fY3B8=R>',/3h'O9uH2OG=XUbXs$[-JrEhRGm#H\NWG$^8=W0VTQ'M'k@tChHrV*?j3LZq@XPI/li3#9_l=hQtE'iqX"Iu`h#8K][/KcitI%7DSlEj=4:N9k0Su41I[nMm9L8s#36!tnNhA.B6:%%pr5shE=s9\F^O*u$no4&6/g*T$;t/dPk9IEE\A,*N*9-b.(IJL\bQ\G=%SmOMAcVW!2kDsaEjmB,-&h0eI4`U?glocMqX!%?5-Qo2n!?:OmLM0-?VmB]2l[V\a&Lf5^[;T?A0XK[#1hZrc'R?9^)aYdrNT:SZ4R?^P;_M3e^IW='R&jCfm'+n>35p=<];\eND+P.%3s!r/>H=Tc;CkpS8T'8[=h)rX4(V`P?699#eE,-#QOtS%Dn8[MYn2uk\hnmGDU6(sH&5eia'Coot?iF>7~>endstream -endobj -xref -0 37 -0000000000 65535 f -0000000061 00000 n -0000000122 00000 n -0000000229 00000 n -0000000341 00000 n -0000000446 00000 n -0000000523 00000 n -0000000728 00000 n -0000000933 00000 n -0000001138 00000 n -0000001343 00000 n -0000001549 00000 n -0000001755 00000 n -0000001961 00000 n -0000002167 00000 n -0000002373 00000 n -0000002579 00000 n -0000002785 00000 n -0000002991 00000 n -0000003197 00000 n -0000003403 00000 n -0000003473 00000 n -0000003754 00000 n -0000003906 00000 n -0000004776 00000 n -0000005897 00000 n -0000007335 00000 n -0000009043 00000 n -0000009971 00000 n -0000011197 00000 n -0000013000 00000 n -0000014502 00000 n -0000015849 00000 n -0000017183 00000 n -0000017949 00000 n -0000019756 00000 n -0000020583 00000 n -trailer -<< -/ID -[<8a6c7f848f6a2a1be56036b86245afb8><8a6c7f848f6a2a1be56036b86245afb8>] -% ReportLab generated PDF document -- digest (opensource) - -/Info 21 0 R -/Root 20 0 R -/Size 37 ->> -startxref -21756 -%%EOF diff --git a/Les07-Nextjs/Les07-Live-Coding-Guide.md b/Les07-Nextjs/Les07-Live-Coding-Guide.md deleted file mode 100644 index 0cf97ac..0000000 --- a/Les07-Nextjs/Les07-Live-Coding-Guide.md +++ /dev/null @@ -1,946 +0,0 @@ -# Les 7 β€” Live Coding Guide -## Van In-Memory naar Supabase - -> **Jouw spiekbriefje.** Dit bestand staat op je privΓ©scherm. Op de beamer draait Cursor. -> Volg stap voor stap. Typ exact wat hier staat. Leg uit met de "Vertel:" blokken. - ---- - -## Planning - -| Blok | Tijd | Onderwerp | -|------|------|-----------| -| Deel 1 | 09:00 – 09:30 | Poll-app afmaken (stemmen werkend) | -| Deel 2 | 09:30 – 10:15 | Supabase introductie β€” No Code | -| Pauze | 10:15 – 10:30 | β€” | -| Deel 3 | 10:30 – 11:30 | Supabase koppelen aan Next.js | -| Afsluiting | 11:30 – 12:00 | Testen, Q&A, huiswerk | - ---- - -# DEEL 1: Poll-app afmaken (09:00 – 09:30) - -> **Vertel:** "Vorige les hebben we de QuickPoll app gebouwd, maar het stemmen werkt nog niet echt. De POST route logt alleen 'hello from server'. Vandaag maken we dat eerst werkend, en daarna koppelen we alles aan een echte database: Supabase." - ---- - -## Stap 1.1 β€” `votePoll()` functie toevoegen aan `data.ts` - -> **Vertel:** "We hebben `getPolls()` en `getPollById()` maar nog geen functie om een stem te verwerken. Die voegen we nu toe." - -Open `lib/data.ts` en voeg onderaan toe: - -```typescript -export function votePoll(id: string, optionIndex: number): Poll | undefined { - const poll = polls.find((p) => p.id === id); - if (!poll) return undefined; - - if (optionIndex < 0 || optionIndex >= poll.options.length) return undefined; - - poll.votes[optionIndex]++; - return poll; -} -``` - -> **Vertel:** "Simpel: we zoeken de poll, checken of de index geldig is, en verhogen de votes. We returnen de poll zodat de API de updated data kan terugsturen." - ---- - -## Stap 1.2 β€” POST route werkend maken - -> **Vertel:** "Nu gaan we de POST route fixen. Die deed nog niks β€” alleen console.log. We moeten de body uitlezen, votePoll aanroepen, en een response terugsturen." - -Open `app/api/polls/[id]/route.ts` en vervang de hele inhoud: - -```typescript -import { NextResponse } from "next/server"; -import { votePoll } from "@/lib/data"; - -interface RouteParams { - params: Promise<{ id: string }>; -} - -export async function POST(request: Request, { params }: RouteParams) { - const { id } = await params; - const body = await request.json(); - const optionIndex = body.optionIndex; - - const updatedPoll = votePoll(id, optionIndex); - - if (!updatedPoll) { - return NextResponse.json( - { error: "Poll niet gevonden of ongeldige optie" }, - { status: 400 } - ); - } - - return NextResponse.json(updatedPoll); -} -``` - -> **Vertel:** "We lezen de body uit met `request.json()`, halen de `optionIndex` eruit, en roepen onze nieuwe `votePoll` functie aan. Als het mislukt sturen we een 400 error terug, anders de updated poll." - ---- - -## Stap 1.3 β€” Server Component + VoteForm split - -> **Vertel:** "Nu gaan we de detail pagina goed opzetten. Op dit moment is het hele bestand een Client Component met `'use client'`. Maar dat is niet hoe Next.js bedoeld is. De pagina zelf moet een Server Component zijn β€” die haalt de data op. En alleen het stukje dat interactief is (stemmen), dat wordt een apart Client Component. Dit is het belangrijkste patroon in Next.js." - -### Stap 1.3a β€” VoteForm component aanmaken - -Maak een nieuw bestand `components/VoteForm.tsx`: - -```typescript -'use client' - -import { Poll } from "@/types"; -import { PollItem } from "./PollItem"; -import { useState } from "react"; - -export function VoteForm({ poll: initialPoll }: { poll: Poll }) { - const [poll, setPoll] = useState(initialPoll); - - const onVote = async (option: string) => { - // Zoek de INDEX van de option in de array - const optionIndex = poll.options.indexOf(option); - - const response = await fetch(`/api/polls/${poll.id}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ optionIndex }), - }); - - if (response.ok) { - const updatedPoll = await response.json(); - setPoll(updatedPoll); - } - }; - - return ; -} -``` - -> **Vertel:** "Dit is het Client Component β€” het enige stuk dat `'use client'` nodig heeft. Het ontvangt de poll als prop, slaat die op in state, en handelt het stemmen af. Na het stemmen updaten we de state met de response van de API." - -### Stap 1.3b β€” Page als Server Component - -Open `app/poll/[id]/page.tsx` en vervang de **hele** inhoud: - -```typescript -import { getPollById } from "@/lib/data"; -import { VoteForm } from "@/components/VoteForm"; -import { notFound } from "next/navigation"; - -interface PageProps { - params: Promise<{ id: string }>; -} - -export default async function PollPage({ params }: PageProps) { - const { id } = await params; - const poll = getPollById(id); - - if (!poll) notFound(); - - return ( -
-

{poll.question}

- -
- ); -} -``` - -> **Vertel:** "Kijk wat hier gebeurt: de pagina is een Server Component. Geen `'use client'`, geen `useState`, geen `useEffect`. De data wordt direct opgehaald met `getPollById()` β€” op de server. Dan geven we de poll als prop door aan de VoteForm. Dat is het patroon: **Server Component haalt data, Client Component doet interactie.**" -> -> **Vertel:** "En we gebruiken `notFound()` β€” als de poll niet bestaat, toont Next.js automatisch een 404 pagina. Dat hadden we vorige les geleerd." - ---- - -## Stap 1.4 β€” GET route toevoegen - -> **Vertel:** "We hebben een POST route, maar nog geen GET. Die hebben we nodig voor de VoteForm β€” na het stemmen fetcht die de updated poll via de API." - -Open `app/api/polls/[id]/route.ts` en vervang met de complete versie met GET + POST: - -```typescript -import { NextResponse } from "next/server"; -import { getPollById, votePoll } from "@/lib/data"; - -interface RouteParams { - params: Promise<{ id: string }>; -} - -export async function GET(request: Request, { params }: RouteParams) { - const { id } = await params; - const poll = getPollById(id); - - if (!poll) { - return NextResponse.json({ error: "Poll niet gevonden" }, { status: 404 }); - } - - return NextResponse.json(poll); -} - -export async function POST(request: Request, { params }: RouteParams) { - const { id } = await params; - const body = await request.json(); - const optionIndex = body.optionIndex; - - const updatedPoll = votePoll(id, optionIndex); - - if (!updatedPoll) { - return NextResponse.json( - { error: "Poll niet gevonden of ongeldige optie" }, - { status: 400 } - ); - } - - return NextResponse.json(updatedPoll); -} -``` - ---- - -## Stap 1.5 β€” Visuele feedback toevoegen aan PollItem - -> **Vertel:** "Laten we het iets mooier maken. We tonen nu het aantal votes en een simpele progress bar per optie." - -Open `components/PollItem.tsx` en vervang de inhoud: - -```typescript -'use client' - -import { Poll } from "@/types" - -type PollItemProps = { - poll: Poll, - onOptionClick?: (option: string) => void -} - -type PollItemOptionProps = { - option: string - votes: number - percentage: number - onClick?: (option: string) => void -} - -export const PollItemOption = ({ option, votes, percentage, onClick }: PollItemOptionProps) => { - return ( -
onClick?.(option)} - className="relative my-2 p-3 border rounded cursor-pointer hover:bg-gray-50 overflow-hidden" - > - {/* Achtergrond bar */} -
- {/* Tekst bovenop de bar */} -
- {option} - {votes} ({percentage}%) -
-
- ) -} - -export const PollItem = ({ poll, onOptionClick }: PollItemProps) => { - const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0); - - return ( -
-

{poll.question}

-

{totalVotes} stemmen

- {poll.options.map((option, index) => { - const votes = poll.votes[index]; - const percentage = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0; - return ( - - ) - })} -
- ) -} -``` - -> **Vertel:** "Nu tonen we bij elke optie het aantal stemmen, een percentage, en een visuele balk. De balk groeit mee met het percentage. We gebruiken `transition-all` zodat het soepel animeert." - ---- - -## βœ… Check: Deel 1 - -> **Check:** Start de dev server (`npm run dev`), ga naar `http://localhost:3000`, klik op een poll, stem op een optie. Je ziet nu: -> - De stem wordt verwerkt -> - De votes updaten direct in de UI -> - De percentage bars verschijnen -> -> **Vertel:** "Dit werkt nu, maar er is een probleem: als je de pagina refresht, zijn je stemmen nog steeds bewaard β€” maar als je de server herstart, is alles weg. Dat is omdat onze data in het geheugen staat. Vandaag gaan we dat oplossen met een echte database." - ---- - -# DEEL 2: Introductie Supabase β€” No Code (09:30 – 10:15) - -> **Vertel:** "Nu gaan we het over een echte database hebben. We gaan Supabase gebruiken. Eerst zonder code β€” puur via de website." - ---- - -## Stap 2.1 β€” Wat is Supabase? - -> **Vertel:** "Supabase is een open-source alternatief voor Firebase. Het geeft je een hele backend out of the box:" -> -> - **PostgreSQL database** β€” een echte, professionele SQL database -> - **Authenticatie** β€” login, registratie, OAuth (Google, GitHub, etc.) -> - **Storage** β€” bestanden uploaden (afbeeldingen, PDF's, etc.) -> - **Realtime** β€” live updates als data verandert -> - **Edge Functions** β€” serverless functies -> -> **Vertel:** "Het grote verschil met Firebase: Supabase gebruikt PostgreSQL. Dat is een open-source database die al 30+ jaar bestaat. Als je Supabase leert, leer je ook SQL β€” en dat is een skill die je overal kunt gebruiken." -> -> **Vertel:** "En het mooie: er is een gratis tier waarmee je prima kunt werken voor kleine projecten en leren." - ---- - -## Stap 2.2 β€” Supabase project aanmaken - -> **Vertel:** "We gaan nu samen een project aanmaken. Open je browser en ga naar supabase.com." - -### Stappen op het scherm: -1. Ga naar **supabase.com** β†’ klik **Start your project** (of **Sign In** als je al een account hebt) -2. Log in met **GitHub** (makkelijkst) -3. Klik **New Project** -4. Vul in: - - **Organization**: kies je org (of maak er een aan) - - **Project name**: `quickpoll` - - **Database Password**: genereer een sterk wachtwoord (sla het op!) - - **Region**: `West EU (Frankfurt)` β€” dichtste bij Nederland -5. Klik **Create new project** -6. Wacht ~30 seconden tot het project klaar is - -> **Vertel:** "Je ziet nu het dashboard. Hier vind je alles: je database, je API keys, de Table Editor, SQL Editor. Laten we beginnen met tabellen maken." - ---- - -## Stap 2.3 β€” `polls` tabel aanmaken via Table Editor - -> **Vertel:** "We gaan nu onze database structuur opzetten. In onze code hadden we één array met Poll objecten. In een database splitsen we dat op in twee tabellen: polls en options. Dat heet normalisatie β€” elke tabel heeft zijn eigen verantwoordelijkheid." - -### Stappen op het scherm: -1. Klik **Table Editor** in het linkermenu -2. Klik **Create a new table** -3. Vul in: - - **Name**: `polls` - - **Enable Row Level Security (RLS)**: laat aan staan (maar we gaan er zo een policy voor maken) -4. Kolommen: - - `id` β†’ staat er al (uuid, primary key) βœ… - - `created_at` β†’ staat er al (timestamptz, default `now()`) βœ… - - Klik **Add column**: - - **Name**: `question` - - **Type**: `text` - - Vink **Is Nullable** UIT (niet null) -5. Klik **Save** - -> **Vertel:** "We hebben nu een polls tabel met drie kolommen: een automatisch gegenereerd id (uuid β€” dat is een uniek ID), een question (tekst), en created_at (wanneer de poll is aangemaakt). Supabase maakt id en created_at automatisch voor je." - ---- - -## Stap 2.4 β€” `options` tabel aanmaken - -> **Vertel:** "Nu de options tabel. Elke poll heeft meerdere opties. In plaats van een array in één kolom, maken we een aparte tabel met een verwijzing (foreign key) naar de poll." - -### Stappen op het scherm: -1. Klik **New Table** -2. Vul in: - - **Name**: `options` - - **Enable RLS**: aan -3. Kolommen (naast id en created_at): - - Klik **Add column**: - - **Name**: `poll_id` - - **Type**: `uuid` - - **Is Nullable**: UIT - - Klik **Add column**: - - **Name**: `text` - - **Type**: `text` - - **Is Nullable**: UIT - - Klik **Add column**: - - **Name**: `votes` - - **Type**: `int8` - - **Default value**: `0` -4. Nu de foreign key: klik op het **link-icoontje** naast `poll_id` - - **Foreign table**: `polls` - - **Foreign column**: `id` - - **On delete**: `CASCADE` (als een poll wordt verwijderd, verdwijnen de opties ook) -5. Klik **Save** - -> **Vertel:** "CASCADE betekent: als je een poll verwijdert, worden automatisch alle opties van die poll ook verwijderd. Dat voorkomt 'wees-data' β€” opties die naar een poll verwijzen die niet meer bestaat." - ---- - -## Stap 2.5 β€” RLS policies instellen - -> **Vertel:** "Supabase heeft Row Level Security β€” dat betekent dat standaard NIEMAND je data kan lezen of schrijven via de API. We moeten expliciet toestemming geven. Voor onze app willen we dat iedereen polls kan lezen en stemmen." - -### Stappen op het scherm: -1. Ga naar **Authentication** β†’ **Policies** (of via Table Editor β†’ polls β†’ **RLS Policies**) -2. Bij de `polls` tabel, klik **New Policy** - - **Policy name**: `Allow public read` - - **Allowed operation**: `SELECT` - - **Target roles**: `anon` - - **USING expression**: `true` - - Klik **Save** -3. Bij de `options` tabel, maak twee policies: - - **Policy 1 β€” Lezen**: - - **Name**: `Allow public read` - - **Operation**: `SELECT` - - **Target roles**: `anon` - - **USING**: `true` - - **Policy 2 β€” Updaten (stemmen)**: - - **Name**: `Allow public vote` - - **Operation**: `UPDATE` - - **Target roles**: `anon` - - **USING**: `true` - - **WITH CHECK**: `true` - -> **Vertel:** "We geven de `anon` rol (dat zijn niet-ingelogde gebruikers) toestemming om polls en opties te lezen, en om opties te updaten (voor het stemmen). In een productie-app zou je dit veel strakker instellen, maar voor ons leerproject is dit prima." - ---- - -## Stap 2.6 β€” Testdata toevoegen - -> **Vertel:** "Laten we onze twee polls toevoegen, dezelfde als in onze code." - -### In de Table Editor β†’ `polls`: -1. Klik **Insert row** -2. Vul in: `question`: `Ik ben een vraag` (id en created_at worden automatisch ingevuld) -3. Klik **Save** -4. Voeg nog een rij toe: `question`: `Ik ben een vraag 2` -5. **Kopieer de id's** van beide polls β€” die heb je nodig voor de options! - -### In de Table Editor β†’ `options`: -1. Voeg rijen toe voor poll 1 (gebruik het id van de eerste poll als `poll_id`): - - `poll_id`: *[id van poll 1]*, `text`: `optie 1`, `votes`: `1` - - `poll_id`: *[id van poll 1]*, `text`: `optie 2`, `votes`: `1` - - `poll_id`: *[id van poll 1]*, `text`: `optie 3`, `votes`: `1` - - `poll_id`: *[id van poll 1]*, `text`: `optie 4`, `votes`: `1` -2. Doe hetzelfde voor poll 2 - -> **Check:** Je hebt nu 2 rijen in `polls` en 8 rijen in `options`. - ---- - -## Stap 2.7 β€” SQL Editor verkennen - -> **Vertel:** "Supabase heeft ook een SQL Editor. Hier kun je direct SQL queries uitvoeren. Laten we eens kijken wat Supabase onder water doet." - -### In de SQL Editor, typ en run: - -```sql -SELECT * FROM polls; -``` - -> **Vertel:** "Dit haalt alle polls op. Simpel toch? SQL is de taal waarmee je met databases praat." - -```sql -SELECT * FROM options WHERE poll_id = '[plak hier een poll id]'; -``` - -> **Vertel:** "Dit haalt alleen de opties op van één specifieke poll. De WHERE clausule filtert." - -```sql -SELECT polls.question, options.text, options.votes -FROM polls -JOIN options ON options.poll_id = polls.id; -``` - -> **Vertel:** "Dit is een JOIN β€” we combineren data uit twee tabellen. Je ziet de vraag samen met elke optie en het aantal stemmen. Dit is de kracht van een relationele database." - ---- - -## β˜• PAUZE (10:15 – 10:30) - -> **Vertel:** "Na de pauze gaan we Supabase koppelen aan ons Next.js project. Dan halen we de data niet meer uit een array, maar uit de echte database." - ---- - -# DEEL 3: Supabase koppelen aan Next.js (10:30 – 11:30) - ---- - -## Stap 3.1 β€” Supabase client library installeren - -> **Vertel:** "We gaan nu onze Next.js app koppelen aan Supabase. Eerst installeren we de Supabase JavaScript library." - -In de terminal: - -```bash -npm install @supabase/supabase-js -``` - ---- - -## Stap 3.2 β€” Environment variables instellen - -> **Vertel:** "We hebben twee dingen nodig van Supabase: de project URL en de anon key. Die vind je in je dashboard onder Settings β†’ API." - -### Stappen op het scherm: -1. Ga naar **Settings** β†’ **API** in het Supabase dashboard -2. Kopieer de **Project URL** en de **anon public key** - -Maak een nieuw bestand `.env.local` in de root van je project: - -```bash -NEXT_PUBLIC_SUPABASE_URL=https://jouw-project.supabase.co -NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... -``` - -> **Vertel:** "We gebruiken `NEXT_PUBLIC_` als prefix zodat deze variabelen ook in de browser beschikbaar zijn. Normaal gesproken wil je secrets NIET public maken, maar de anon key is ontworpen om veilig in de browser te gebruiken β€” de beveiliging zit in de RLS policies die we net hebben ingesteld." -> -> **Belangrijk:** "Voeg `.env.local` toe aan je `.gitignore` als die er nog niet in staat! Dit bestand mag NOOIT naar GitHub." - ---- - -## Stap 3.3 β€” Supabase client aanmaken - -> **Vertel:** "We maken een utility bestand aan dat de Supabase client configureert. Dit gebruiken we overal in onze app." - -Maak een nieuw bestand `lib/supabase.ts`: - -```typescript -import { createClient } from "@supabase/supabase-js"; - -const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; -const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey); -``` - -> **Vertel:** "Drie regels code. We importeren `createClient` van Supabase, lezen de URL en key uit de environment variables, en exporteren een client instance. Het uitroepteken (`!`) zegt tegen TypeScript: 'ik weet zeker dat deze waarde bestaat'. We gebruiken dit ene `supabase` object overal in onze app." - ---- - -## Stap 3.4 β€” Types updaten - -> **Vertel:** "Onze database structuur is anders dan wat we in de code hadden. In de database hebben we aparte tabellen voor polls en options. Laten we onze types updaten." - -Open `types/index.ts` en vervang de inhoud: - -```typescript -export interface Poll { - id: string; - question: string; - created_at: string; - options: Option[]; -} - -export interface Option { - id: string; - poll_id: string; - text: string; - votes: number; -} -``` - -> **Vertel:** "We hebben nu twee types: Poll en Option. Een Poll heeft een array van Options. Dit matcht precies met onze twee database tabellen. Let op: `id` is nu een `string` (uuid) in plaats van een simpel nummer." - ---- - -## Stap 3.5 β€” `data.ts` aanpassen voor Supabase - -> **Vertel:** "Nu het belangrijkste: we vervangen onze hardcoded array door echte database queries. We herschrijven `lib/data.ts` volledig." - -Open `lib/data.ts` en vervang de **hele** inhoud: - -```typescript -import { supabase } from "./supabase"; -import { Poll, Option } from "@/types"; - -export async function getPolls(): Promise { - const { data: polls, error } = await supabase - .from("polls") - .select("*, options(*)") - .order("created_at", { ascending: false }); - - if (error) { - console.error("Error fetching polls:", error); - return []; - } - - return polls || []; -} - -export async function getPollById(id: string): Promise { - const { data: poll, error } = await supabase - .from("polls") - .select("*, options(*)") - .eq("id", id) - .single(); - - if (error) { - console.error("Error fetching poll:", error); - return null; - } - - return poll; -} - -export async function votePoll( - pollId: string, - optionId: string -): Promise