final lessons
This commit is contained in:
99
Les01-Introductie-AI/Les01-Huiswerk.pdf
Normal file
99
Les01-Introductie-AI/Les01-Huiswerk.pdf
Normal file
@@ -0,0 +1,99 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> 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`O<CG31[)\e3XhUV3t<8(!8(^RP&DNu^[1Gr#u;8skju8)H<N_flr[(le"I482UJ0BCK(/dR_!PeHNJDJ"%6\e47TGL?6CNR1uMm?N++moEO]6H1;:*=E,`=cB3)<g8$a^jkkYF[WYr&6QRoNWF=.TUG;[%g#J$<5]9YkISTY%_+*L\"W7-JN63hmh?_)QnrLX9)WR"!EUV;l9*Iud<]eN24tptaTJSP32%k_0V[BV0-58F>K>+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.<g9?hYHB2124WO,P![inK"ZhuHBMtYOc,Eb;V7W]M9[cG*+3hQ[nM)ZUd'iO767&keiE.4XUPKp%!.E'79SSFEM\OLT>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'!<?KX84[5_H.Vg0M>&%q@LYq9Z?*QUf+Wq\Me6T_r*u2oIR4<ZOCaujq*o%XX8t@?].!<=8S%,*AEn0rc&?#_s#.;W7U^X*9klm3jNkA!)*StDeVm_0COEts'Xs'GO3D+3,c3r^t@`.!mAlSgEiG(n:1aZ/AHO,ViQF.:9f\Rb&1"uRRu4#j"oEl?`3K8VY6*30l)_ub,aZXqj8h/4Zm4A@LpY]D*o5Nn/1V?rPUHAZ:h&EgS;7^=$$BCQ@o-bNhI'DU+WDU*dD#m=5:m(~>endstream
|
||||
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<r@^Gc;jfRik\$lFb##FI1J['&>.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<S"hB)oLJ.Cha:.V;3B8O4"4(K,]H4Ij2rh_MiF@gq)u9gR';kPhpaO)4:FkRD2TQRYK.<fD6Y/8YSfq^#D=HO&GG0:(@2FZIkRUrXJ/g,L17)f4#^m[g&JF_6+,ccSMOKVr#d,+Y<X3Npp;:c'R^mE)hmVcR9T5;U86'XYS%3M2IFY0'iDPdc^<U$\NKPRVqh)H(8M0iadqhm)TUGgB"']fj2HR/_k3FlYB'tuh99X!\B3F`FPcD.>@YW-pAnA28r&/;&<UD`MmnmI8Fe8`:L!`.t^D`Fbem;>$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;<YX.[_TOb&NHirI)fq6@%&BFici<1BFX/q?(8+Pn_21hbmdUds'208$Wu@e#DTqU?JnaEp'9)?T/#P+8"E->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"<O$h#BZ+)%7N<@V&JOi+JG)^3.XAG1b+/+o\&[<D][eRlUJp6L"O3VLO%kf.HpeK9(;ALmF!^4O8_;^*X95X6VE?4<Q",7q4?j_Kqm:W>>(*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<Z'K3V?L_b#R5+-WiMSCdK\k+/68n!]OcT&9XZ5a0[[=B%]HpfrRI@"XdNcgit]>$`mgRLX$QBOPRuWZD=5LaX&lXq^0QDnLHZRPm"FXqb"%UFV([c7CgC&(05lsW%$KZp7c!J0V)k&E#E<b'Yr&k4iNY"8>URuSmjTb#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
|
||||
[<e6f7de6a0b8ef06b94ec03bf5e53026d><e6f7de6a0b8ef06b94ec03bf5e53026d>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 8 0 R
|
||||
/Root 7 0 R
|
||||
/Size 12
|
||||
>>
|
||||
startxref
|
||||
4765
|
||||
%%EOF
|
||||
193
Les01-Introductie-AI/Les01-Lesopdracht.pdf
Normal file
193
Les01-Introductie-AI/Les01-Lesopdracht.pdf
Normal file
@@ -0,0 +1,193 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> 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<Qf4ZfO$Nrd"()B9;[W36YD+D#c)*R><_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@SV<JH"MIm3-rIcA69\3[=&N:#S*!nEH+mcco_4*mE9Z0-YWZV:<tf&Sn<kt9IA[`s='D?3Fs/Q.$dL>93Y'bSgGP=#*qu@j`jrk>eL536!i'fu2JQgJMV1TX>@i@oOr6!ffDFkE]h54ZD@B,gj&-5BHkunqX+H2(B\]6K_BP,.-bI)#O."F<AF$kslWoLUAjuUR>Vs/R.qU<VCmBFXmol*r*6ojK*OD/Q\51Y[.OtC#@fOP1l#:cj&?nQ.+!(,S9lnVn(3jAaBmTac^3$O[D?n;L"]4jq?M+J;Y@O:$Jnl;rn7S$%a5@/[*"o)'\ACNQ'aR_hW7LAf%[-)@G!a0qh9ZkE@uFs:(W3=lYMqe_R?b1Sl!^Xa-:q;\+\.jul^`YnV9RW.XG'W5nEbFgj^nG3Rb_8eI"IK(rD<$3>s*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_#kKGt<?1+fd7Te')%`,M35K-$An6[WI_:Rd;W%'b<P3Y8LCk$%1*VJrSH@50^768.MHp+7"VoR.PgD?Z(h02537djmQt>N(<pU,\9:J.0&;O0U7NhR"0EQNnJk6J"0pXlL3XeG6NZk$'&.PD-b($3;j)1Jn%fGi\T?+K_PD(u'ci'c"`rUIO'!cZ&.JY2TH)<a^kNr64lUe8YKCP&/9:^N2+dg\:`h9!-2iH.g.r9^T/3&C!/Rd)F_m8oXD@k662LI.q>c@co8n2e>-s9@7V?C>NNqPVH^N6W,6`F._;ho.PJ2K`(,]L[_Urb$W<R9K*e2g.D"Wa6$QDrtB%2%,$Fndi[6TTl$L06>S13cO>-A-"0/PSPugX@aFDfFh.f`4TJ;*E03"]C3_=[DZ`1Gt(EqsApl=QWe%Ne5_!Cm?X$/1c+c&IMRp<4cDc<j/f)s3?0Y=O72&j;`SN5)k7QoqY/!-)`nd.bFc&F)cl`)[/_#`[E4R1f6rIqEBGf\`Iuk*^T9[KDggXG'Y)Rd8[)i$YpO=W0*Ne+o;%o0KNicK3?o\+f^-m,4?,<UO^_<,?tdm9[b8]6'sR+Bn=t1_dA?Bmjh-Q!gTTn?8p.%5g^/^iU`F+W(^9=-SkA[hRNIl!kl@YW$%bI&>dL/^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<R8Ik&,Du7Y&="?/*_orqb*t?jHoKE_U.d^r0ZdV,(:ZPI)<GHLpE?1]r!WL!pD>>X3K=>VZWA.qj./D\a"Xn>-N:;77,D@"DfoOH6IW>`X/]#WO6eB?HP`'k^,B$3WNTqqQrH@JJ")^jOK%Pm!tolRD;#k8i[k6)l5P4YPH:IYgi<GQIE%kQkf!+^oHt?Q.HcFX4<(latN760m"mbm1c.,:oRoed6^UF,\*boRu_mbm-5K"XZ:Jr$?G4d!]I>C^?</c!3d8[tS-sTsI$+2M+!K-r*,@)aZ`P(KXKsZ%V6DS@Og\(sOVOD'!r_U?5Xkg:53JT/,BRH2o`oqL7hgAhC9`ZH\$fV.L6#AkUn9>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]Bk8dNqMlA<stGoK/P-X;/sdh][rdVY=jc`J>t"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&0hom<VRF/N\K$D/1I:VcKRGC8usq9mem_ndMMY.9b_Yuf]"gUb"lYsQ)o<7[2Bs-p%,2\1nG7Q)mjotJU_2mm=s<S^:tJ[6bNN`MJq]>C?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&(<iY<I>KT7@sa'k>^j=3XN<*fO1jKN:=YkdU/he7i?Y1`Z3$'18<U74#gU:+/VjT/;&Sa-)I,%(U;_A=OD/+\Bs$7Zu(?]=u4i$kn;CU8QJOc6ZG^k[M[KQ*40Ik]u@DQXrMVisCi4c7l(u9PdVDO1H/mk?L'gF@tpEkQR50,)Ka&^TZdq\f&&./r>Qgns;1LiX@G]WG0)6<]uq^Dp'IikM\!s1G]O'1tY\_:WY0m`n'nj1G%k\#nF@<g"f=EBKZ7.1;Q19+2L;0*(HBo5akuBQn$DK3Vs\ks.u:t9P-]O%.b%qj/tT-957LOS0^9Z%X9qPBPNuhOQQ6<BQc'*VH8Y9W1sLafB0egef6>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/)<F3-2be%Ulf#kO1W_Hj`1)-!'8qeiEY?%Am2B/q#s==)[%TDDB`hRMRZiL=q#mi!q.G>o!W,^(5OEPqFj;#G;I8</$"`%`g<a`?$>r4)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@9ja<pI1V1-+/!>eB[@h<]lqF6,6h<.EYa:JM!_R[eMgkd'2Cd)`H[q%<BnaJ(A%A+j)/:_b)g5cK:BK!!V$\PMn0bg@WqZH-"MXNDl[m,=0Gcpb`Z/o'lo]G-QJ\_g:EWftf.lNLgDM`hN;\#bm_Tp7+R&P\!V)Id8^P$=8a\/jTCdO9oApi+()^h`c4miUl4ib*VJ^H[;j7'+,RWt&P2V*qi0H)3&5_PaAt;jb@S2Kkd8V.oih4hT2hNK/%mU^Pn@r@qc6cA(f"@;"Z7Pj?F2ZeAbsj<L*t<A]^ckfHJbX6V?i]pYs&Vn'Op)aj@If'iJ9?>IcO7Uo$27h&:+V6QMqH:bC+.KYT.>u[:NP*:bXI+Mrb:&)3Rmg6fs3ouq0B2jb'bNWau!aZkO0G1tP^`/f5NEX0%-rULhZnS9)\PYd"a4'8Y<5b8He3TWu\H9oAWju,$qc-jRbNEY(=#h<QPc=$2M\3bOVrJ_Wm@D^7'jeZLG<'OL(?6A."\[jRhtVn&CMCLZgtDmUk(cfQE^YlL\_PtF^&YisH%40N@IH[S2/"4mCE1M0O6VrK]=h^O8E%luA%K$>(Pql(KGu["$@B3'lI6j,1g[Ul\<b&`e&;Yg'=n6f!A[B=4<#N5"B65M7/5rSW2'YAVhX"gHYV)R4uq)<"[BgPD.D)_;8P:/_XBDRl(,]V&C,EQVP*O.XpU%RXt$JcF#5U'$+6MDYr3o=S#Y_+ZI9`jIA];g=j>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@CrS<fG/i9Oe3@,R+[Q:_^aNm(Kj-inAA7TY9gJ+7[hQusL74N=W,"2!tO.Q!ks$\"eo'!gNre1pKp!+f4cErcqFgYE7W!\1nHAfb+_"gb4S?J1%A&ZVImtA<V:'mlL*7Vjaq]P?`RI:e7O-1FU[)t\<0do?>pI*"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&<Q77\qC-2XNo1^TkL%3uDoXo#?2(3u%TX%4a^_F&g"B-m?UN'E@r6\0kM[\THUF6Cj)`;ppmSa,rW"Rr8r-gr'p#]t"#nLP3^(\"SSnEn"8PQ[QgQ5eOqRt0%4bnoI2JHVTKZq]6aKQ^aWJHoNF=CKhlJ`(rBhuo-_\9WM\J/(mB#X,CYa/:(d\DO\B+h4mlq38<0hqu?gRdf#HD5+p%lQ:Kk?K!`6VGD=tbBEg!L[GFO^ONgYLCYls`F,WD$=@r$#=Aq`639,0LUVZT;jHnN#eILlI;KaeH`6,^QLB.9_hVRf6=j$>V@U7KZ&fC,\VFEu(+2nOU^8VlM&#lqLY4UXG3YkeIXR$q#1"iHo%rH,.TC'odJBS?#$qNeF#D)s,UsHjf6;9gU"=<m,\6B-5s.q5j?/5(.p`*)#m%SGf3G:ApgqP=fjtt//pY`fY(>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:'bqpkf<tlk\fT:<,B'ZG^.EjRcadESt$,cU$Mk#$re\K$@(E6i40fBo6,b1RBcsmJcT@.Tb3Y,W5r:AGoND#]:o(gM?G6(d7A*('k$`e`SU[=DG]3Z7,0,S,3HNd;pEr=iNAIQThiO:3c="qNKBdu7d.iajY1)Q.LVgL^1!D$7mb<dPs7\t7m]qR<]@@c!Cr7d#c6.1'p!Z2VR>iGrWt+*cV#hC%F-AEG?e_nN3,SB46n3Do>tk_ldL9$T]ZHodNDi9nf9*"k187D#)B*R":R*s757\<Y]Q2i4/%GNXs+Sm0+M&\4a]G`ZaFtl.H16ieJrE551')9$6-7B&)1p&mBHQ/&e94If,UmAlF7\laSM1[Se9^pd(\`4BoQt1[>L<i81App;N.!Bi=c?"Wa;:o0=]A93JqNX_qtL53JLM_;Zd!Y"_>V'Ze.U.=6gIPB'cSGX7`(5ZhNfHBa5Z"QE'D]]rJ2ifk.>WSC<STORA^T[mEI+N9Rn9km`kDh^b:;q.e=/MosG:D+L:s9ZB(f/Bh=MgC[iD3,)d%<NL0O#(*`;ifO;gn$f@eQ1^AnG.nCs2fThbikE;,+r)DO's%J^m!X`6tcX(C0,W6n5LrW6'm%qKD11"@g8Q^UuPEI8TVE`i(cTlD.Hro>Bkmb!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;<RSL)lJ@kk8C05<*d.4`asTq_]IJmk(M,hnAE.kYnn2oh\*GO6>N<GFp)q&Qg'NEC>eK7&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/1p<A%`(\_6eHXauT(@QnPL^qi7K%R8A~>endstream
|
||||
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'<g?8@!%iN7RnVIF(n!SX_J.Ynd(O1/tgP3`c*UO]HkCq[fQhUUk0/U<f3eCKu8JnS6Q:f\*D9"=^f6<oQL$niQ5JMn2EeJ]pQ84"e.`5r$i/062?G(tn\A1MX0REs$CZn"6#HS5S>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%T<:i>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`<<Q7\YuMpkb;\e39L4ND".GpTu4b2)rHm5sBpLEM:@Oj%8Xe)OR+d8SEF!`Lts"@4H[FRFET-JTSMJXW>G%^NX4.(k%?\\YIP[P,Q#W=7#aq/o<:[E(5.kPV&Ki+L7XU`TF(U'dn4Q5%<mnTBpGO&^96EOOo<)U[sVbcDW9T,k+IV\h)>ZK9^e]Vf7u-Afg5)iGVf'*]Bu>4M<iUm)R7V&9pMDcK`GCj;n<L4r="i?9Dc3)E"ZeKa8^kcX[9SVjA_h2A+/n"Ys/U$`e<;pBC[;6BA??#,Bq%sg-Ms0$T+c=+hZe([M*]n:q8SQ[%qlg.Sl6S@,s;\@]!40mK9_?7t7r3:U7T["mrLJd2Y7d)g?^&#("f`3a`f@Fm'PAnjAY$JW6]=t;WWc%-p6GMqN&"tMmFuu&3,?5H+4r;>#87OXHofB^o'-%drJFfp[ILfj9ATg/M1"WbX??SRDTl\,X.EIIG'PV<@;r3JTFB`/<ks#"VU/GI0o9YCjUZQtS;DI8ob6XQ')j*rpDIKTL1SdlWOGf)r]aIUb\(g.ehV63#NEBH#"dVg(<&2^t>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]bOG3<qEVd_I&^PYB::JbV/Tnt)g,8=7AVa6T?!YqI;q1I]:=7Tpm@%d`!&L8sT.48-.9bmaBiCA3:V6(8WUgS+X03l\W:ps1r@T<K?6?H2(6]ocKc8pPUm/Jm`?0M-IQ&qB4OPRm&^>GPQA)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=>2HP<sY_Q?[jTm3$aN*E@t1Z_#ok7rV"^L.s(4DbPQWi"2(,?j<Fm^@%%1a9Z$EXI$fqCN0mEcPQ<I&o))SiMgb=#;iDV#'N;8m3CI2pU/6H#3/aLj!^Z.hOG@3To!]hR/6#pm8RN,@<2=]gqb.;^Td:)2MBB"Yn;A:"R&LNVU:IuDN+VQR;Hm+-162ZK!7Mk+s*V@>LGN4DS'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\<j#;=ho=$#I.Zbm/cDE_(SNG'QB%NeTI`aRYgoI);X-8%7s=fIU?E*Yn"qB]1#u7]]MeLJR62cCBGG@VGh<t;p#!Xgg66!?ZSmW/esr?AQmCW^p!UI\sbBICl-3:nlUXI$,(.`2P+-bCl]s-_;!Y4*I?Tad+fD.U.XpO$67g@-O=WPiVus@45G#G0mrb19`eU71Y3Y)]io;6"EAOE_a0H7Wfk:#2IO^DEm"g7$%+W,$IKjnGCKmsqqm)kTKak)28$r^?Ad1c1u?tX]5VPI44&P!0OF37fiY&!m>TRc]$"gsQPp5s;D-M\^!oBO:#/eq)R:_IGgf],BfWP3d<&$c)P<rkq'puPB*]\g-HKnb+W[a<q4u^Zj)8aI'O@@,KiLko8d?`t(2C2qOm6M3oT6.o.]jNV-fg'uQ"cPO"k5TNP=MWod%F]6,l\`+M3j*!eufIiHgK1X7T\,;E]_N+AaYHD#kK$7lfq:kXNC[3L(pu]'5G1;)"JPR]M+5>Ud#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
|
||||
[<e1a466ea5dd58fb3081b3d5e8e1860e2><e1a466ea5dd58fb3081b3d5e8e1860e2>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 15 0 R
|
||||
/Root 14 0 R
|
||||
/Size 23
|
||||
>>
|
||||
startxref
|
||||
12852
|
||||
%%EOF
|
||||
376
Les01-Introductie-AI/Les01-Lesstof.md
Normal file
376
Les01-Introductie-AI/Les01-Lesstof.md
Normal file
@@ -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
|
||||
238
Les01-Introductie-AI/Les01-Lesstof.pdf
Normal file
238
Les01-Introductie-AI/Les01-Lesstof.pdf
Normal file
@@ -0,0 +1,238 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> 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;`$5J<L5lO&n-!fcL]h[%(PXJo6GiM#O+@rK1_^ujK)'s'p=?iN^c@kW*'X_'h=^I?K\g%r"SM2%S)tNPT`hD^=fnji;!*Nso]L2`lh`?en2?o[E]D>8'L&Qa%]9X<S;6Un)ICON9_3rh/tmb"rFahL9;r^5KFigk8oJ6CWdJWII#'.A('A>$pkT>r1^(JJ<o>_\N-lG0n"dE/i4=3[$[ooAKbPUQLXSN7[($U!bF@YpL->FLR-RT+B6nDN(-$3Q,-]c<Mi<lXAk.Theo]WHNS+otBVUQl@?9D+;Z*]ZlT]\0,H`umTi2sY"EtG>a1uYp&/!"'q64oV<P"l;A*9OQ"nb<:SO^)P2.f?8G5G[PgC`.QUQ)PI`ibU&NmF)#%5?,e5)V*N2@63E-LRM@5oYAM%1Fdok=-tD0qVV4EYh't,,J_T$!j(%!k43LmD,pj!NYL:gSm&N,0mQD<-k+7+s/Cl.ZpBEV=R;kE"a$$M;Qus)<3sMnrZVh#I=OBKY"$gQF$g3G5E>i!ag;]RbeU-gS<<M\Lh?]ob99N-&B_ADO1lm/r`<+=t?SPiPh..p=%TigUi=sQo#,il=Se^Of(.]C_Y3,VW&V65&P5o'dmktr?@NK.*3eH6TEZ6%YOrW[0$c6-UE$g/BVQ"0TKYnn%7SnUL8$3Vo;K(:1Q8GONM&ES-2))2E-KkINc%aQu2=;P\Dn.[4GoFgU$csa1fea*tLFcYY=HF?mC2UC*Ts4`rG`?%^)ja-=Dj(BOkiqel)9b^HsSoXhq9rBUR4d%EO:V(-/K2D+M3fS_Sg#@&%*g2;1\6oaJG^g*+":g;_RtUA\2-0<Mmsn^PNf]Xc@fTMC:Pa$DA)fQD$63]k&-Ifq[HX1!tVke#-;q/URr;mq!n2e^l,\p@jr,2C6+oDVEocG;Yi[-mEeVB2"RJ,^Q?84n"(5D+k9(g4n:,UJ;2_<!tX#V7qC0RaG7i'^uIT&_T8\@Hld%PBVB:/WZrhaW@E.iFMIo<K=LG8BuA1%;jiiP3VL4@02tB;GZ'?Wl;7e%f6Ec".'B<3sNgG/NHTJaFH[S"C$L:=J;9ml&hLQ0)U[a)R6fFb(Lp98,Oc4H=&HoAGaF>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-]<mgW)J4AE/ZIAbiSIQPY?J`lTok;n1*eSc]:B%n!mO/&9D#9rfiE`VW>d+Z"mRSek4P/!\$l%ZL%LQPbW0//,G6FoP<uAU/jiD@.djau$Yu8UJ!=X@<*A#'J+b`)Te[/%t(G:5\*BD]`'UY=qGG3N_]jR2Nkt/k`9Y31fA/>iCsFW4\2"@hg)G["c;\B@1_C3,83ViR%:up_/qQ?53a@lFos0dc%q&"a&ih5C2sEUj:/Q4kVf&@<caT/ur=`DJ\\jljU$mhr_C.\d6L'L5U4K%5.'XmB*8e,u4[b>T1p)Eh[o-S4N6C15m5uncGH`cm+$HTjKVNbB_N9^<3:?(A@4IkD.N*#DVa=quP1D<X&.C7OVA2@Z.LfKRM-K@e4_^C/3=(:gHXtUK.b-8FK#c?*;mm$=AqB,~>endstream
|
||||
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_,<p=Ps>/mD)kKn"2b`MnD"T_=82jDgUELIIe!!BJ5,pEh^"oArsO-PJdJ)$$29*SUa5V`A*9:+Uh`F0lZ-nNKX@4KS<89XZc,7Olqkk^^6d7IJ<PkIu/0Km6h?NaNa">2PV"'&_(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;b<S(ctUb$7V[+GKr9&G5^rYmPQp*6cCSV?DAkA;D]KiuI+oL2p*+"m3k0BWScW>GH>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*?Kd<Q%juBI1.eD<Uf<%ILG3e?'PI//rVuHR__Jn`_"2.b;O!Y<5Ka?)ndb+)<-5,7+adQHh22.u5B?ZE$B9+r(D&m`$[,<FZqQ]aDI/SVc[cR-,G^t-W9!Tj!cpZ3CZIhNi*dH3U7gJj@p.uR&Gn)(D";6keV*]%mB'V:WNhO8`78%2Of*pM0UHO+E%S%1QsHJL0:#9_W@6X'Y-D*WNT#q6ikT?*;^t>Gd';,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`&PV<g*c@6?,OVbT)"/7JuM9]Yd',A$8982*J,9-5HiK7I-A[=Xh(d^$"<7)\e/MkeTktJ+jVKU<YRd)eUKj&i>6l#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.!\j<h'hWm#NEhqkUiILIBJ_SdpV>5!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@N<Ncfs3[jS&teI62C2@SY"8a<hZ_qN&el,U^oNIUkqaKfiaOi0#$_/.A;8Coe!G/"feelIj*"(AKut?"0Mhq4W:J]u,6P#&BQN^)pWfhoHUKnKR*SdQu+R$Y"@uC6\"^3\KHY(O.d_B_LT/pTNe9@P+Tj;\U\ZU.UPa'OB@75[V:#Qf9.9@OK<r1r#j^1X0;meFc2-i!7:jcUCj5@T=(e)m<foP;=?N%Mik0pi[l_i:nkq\-pJ!"&o.]<e?O93>eshg+-@mfj%L65)OGN#8tu[WEjXiHFcaZ[49=X6%M.a/1Il'<GY)V5DkUE$$<dM4<05heNU]r+-Fo.Z9c@S8GG[PTq34cZn'*n-;#'Lr?D##+,FmSa0@Lsg&SfP#)8o+2tE,"\eVRf\L:r6"`<A_<maAbb)3#d.?EMPi\VBl-q_#cA7VbIE\9a/n6;$j^kca412LQ(=GN9+Q[&pq)^1Qu6'S#jo6'YB/!AJ&<o@]:-UY9*24A"^_3QZK7R'/>Xjk(tE'-enkB.>I!g@UrGh;khA+nmbe$Y^$<:&5'U:'?hN:UVCjbQI=Qmn0bZlSk:\9JAS77i9EPhIf%#RAZ`(_:keIDIIP76V?o]@T<@'6o+fB=<TkLpDjV"Ej9ZF6UUP\R*oaFU6q^mdaaF'=R?TPAW"YQtHYqDb.9W%L\d\n>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?f0d4i6SQ<G2GR--MjASA^tMn?=-M7=VuSKKaGT$oNSeWf^``@UYQ>RSK.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%<QUHj\G']eCrS*O?2fNn*uXq/V\gOL2ZVagS=N3<U>#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<(<E/"[RU-UoR/Zeo9T.8JGEM1;C*pTS2<'O8Q.HACLln`^a/K`!r+@$80<A00":/k"!kGM"ipAL(EVuu^N5dqIb;'h/p]K"e38H"cr="fpSM_:Om28OWn8h(3/ch!\mKR"i<.2^fN[=JaE`76\uKPdFhl1s:tK,N><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<O\#(n(/Isi6AM53*X*EDnL8JJ-9d!0eK][qZE*e!qYn*(2;a"_u9ZO_Kbiu.WS)o;;,]GP#RIGkB%O@Ct7@HN8h!,`k30@9:u$$"rOH'%obH)[:0&O`&UAm(]k5lK-\:I-5L^:EaL9bT2),&f>&: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&<OD5h++K0A01aH>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'<nBOED&OCtQi)Lr&+[AOrh`DSA#m#d0+:`;L^@><n%D$#6s*O=lt+kDjlW1#lo!>IgGR.>=/YPrNCh&XYlj[RBZ29A5_T>I,:f&?Edi<=D5:qj7T>Yp_L]*u=+YEt;YFGS%R<U97&fT&bsJHgT:USg2K'o8mCFbjEYA]7>N7K`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#<C0.P*p/@!fI#>0ibj&tpMKjOMLe6*Q?UlkbLhno48Q]Z1&D`8K>B%pVr<tE/WrtOsOfZ:poOtoaa5pIV:mMe:E/8FT3,o(#kQO-8s_tS7g^b,J3&&eKcnH?<<o<Y")T#[.=hON&OlqEVQZ);nCB2>R1: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;L<aoZCQ<[!2M,+sY@pq"MX^I`5+cXfN[d,@b<$agncU4;QbXNOgVa_Ra>u9BpL=pI@]ldNLX#%<S/V-scgWl.03%'aSl#]0\k;U_*ca/u;^1rbYI+M`Oij7lH40[0o1<kmng-&U+1k"7]S.<G'WF`K7frR5irc6TLkJ$8-Bg-2^I[p-A%ShG$[X7B4O'W4HP,Q0W8D=b7m8OC&kQT=)R?%4&6[K[:6ud-T5!CB$eJ/i5F*gE4YZ6!0'5C/t$`1t?:jpn("gXuYBM0dspT2+s]6ZQnU*)>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<Ki(b[KUks`lhm;0$kRm"]SVVTR@+<QMY4>^TB5NjRfDO,O8G<=]E-o^l9DOr_os6TW%;)cTS]ToMOi+`_2"PGJU8J3<i.gY3.ubK!:keK"0<Q09!XRY/-KF;!WLnoReVs7@iZ"0Q_aA4=1c4;I(/OS"Dt<@S\`V5<HYDrVn8bs&l$RaE>"O`g:Bf3lcBHdi_?a?1HCR'IujVKtnP*c5k4ToYC)_h8"12LZcQ0>**L21W*aumAMs\SH6E"irs`C>().ZZUAad/XHj2%4W'p)*V2s3%<qmO4AdGhun\46X-L-4Z)g-kYD75/"nK"6Jr3iIroYo0QGo/on*6t^+cu@?#R@"A#`UuA-pLL`nOZZh\ge[;%q?D'+VNZlNf>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\tNZSGA<L5S"lj"/S(F=N"I!,p*I[g!A?5]&p.4bGb]?24+B\;F(Urc>3..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=<W^igPod8/s-S,O@!FP=!2li/t?PM2&>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-"4n<O/B]ee+a7@9CR536\0%!&Z.*qDQ=de)YoWPih_QL,AJ5<h\,\iH=jg"<RI5.ALknCqNVr7X,'lDFQZ"(*@N(1R8qM^#^X4]eP<Vip3/r8VQ\%sJG5<osrJpbBApg>ORg*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><k?R:$S*)W<Ttq*q_:G8NRN[BlGB_Ek\?;qT21rOXA#TU->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%<J=3`0l4T3.;pH\(!'j*pciB"j[2&LJND#1EFsR3CCBn@-oZU'C[QW'9&iR69pAJs*E')jRiRbhomYF(#E?k<r*Be/9RUU^j.UHdEq0E2"IX!h[+8'S2X&7GQ9i7_AoM2PhN=p8i]`dEHc8aCnR&&KT\2OG!\C=[#^/M.WTXUIa[!T3DE67tBWU>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=><n`U$Mj(=u`J[nk8:NN`!W3@rPSX$4!;5?+8F4fSrW=+(dI9G</%q>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(TYORoOt<aOOMoTTW=Z`&JL8smetV'8E)$ZN(CPEpT(CG>Ye<tD]eP0%3m#;OO`H%.d+'q0OO$./I9u\rHIM4>'D".A*2bUE9gF3L+cJ2/\TRq1gI"IWhJl=.AuaE&s)2Q,A?bsS7,A>Y>/iq&,@aEW-hfnr+cuQHNQ>&p1buPr(*\JU`JTY3)#\*6<I:h$X$XmcDdtCH\VmEam3`V/s\FEn6pCUrk'4@@F.ToY"Rcj)kpZE^0uLVdl!ft<I<L$RKYMYK7`:2qRRB1QV<CoI(K)68OqT]/"@u=hG!p!R<OcYmrRs[AKrmA(N&Kpmh[[1T.[QZ3k@8ZIeRY!)u1;0>?`6j2LEM.,U\K<JGX2elsSokCaKouD)Y%f[AK;$R?R9b>'$4iloqVo2D[M</\Dfm3NYp@FR0uq9kXu'6hfIrE\bQB&%'H@pSRi#nN&3k-\9!(Qi2R8k_Q)^X?&u,q2Y&^@Ged@m%a\Dq*/@^^EE~>endstream
|
||||
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+2iSkg<N0G<N1BIDa>noH#!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;f7Dq<G1h$T+Y:UP-3H8>Wg9'9VC.G!+)+9lp`k8o8#nlhLV1cF%4K%U+`:B*XUCX4>VmFMX/LY*2978R3cj[85J;-X8!fr-q31+6<ChL:\kSD+HIFY',9:o3*tu1>a6L?52LkkOts&O;&?\o(t57:YbPdN0i_=,^hX5lk@HYe"%9[c*Z,*n#:WLFV%/Kn1juXD'=1,u(WqE<PP6rYDGGdP:$!2_YQe!N%IauB6nGEI6Qf#E/fC=JtM(ub3M*Uo7_EfcS6L&$)NS3NF7L33&n-sA"Ys5mQf;D+]WB*lR[LQ!KP#L[Z;F)]WY1OZMP9mZ&4@FG5KS3)H`]7""Uad]%L^ETAL,C]&UIm@m(fn=7CNs9pbSo5]EE5nnB#N@H+%+/L-/'T;7&=5SJP%=R:ut6ENJ)VO$?]?XorEgAZp-2.KM!EXrH`%c^_m:!o`AL\>^_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%<jem#V2Ad?&$kV.7b4'J24Dbp3W>4uX>,]YUeMV&=oVKhuXj?3rKaq<8"L_@(;Qbd<tD*0Nlqr!(,5>M)$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$.<bg/A'&MWJk^keBeU%AXM#5^_,hF.oPq&p=W0`T8UU!6VAr(0gLoSOAGu,%2ih*Ro:!Q>"J=38(4#&8Znm\4^u.q9_J]Na>(WG+r6#!FKT@(=^MOo/Pt:Tk23oog\)%o_:1D,D3&9#R68T>F0%/0H+bEnfCi<TX=;^0++rPLU,4<r(L`#XdL`)G06bWW2Z~>endstream
|
||||
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!<n>C,Y9@,+4%p2(R&Ir^Y&(tM'FrS!ceo,N0<DEo5,lGI;Bo`@2*je5<k2^Z/KhqBHA22CgL3PGBe1*Q`IOsa7[;/V!aUn5Fd0aTsndPAU:LY:1'"Co\@f]n&KNh6>q+k\XKaO:>I(9?e>gR((Hu@&>FDld<Ai]7eN6>5)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(n<ga^\`e$>db4'C_p>543]]CeIf4ZhV+%T"#A;@A!;,*&"*h6*SH++1ce4O>Q:5Mt.&foeVab^;$.]3Q\eh<SMYi,8M,kTH-(iQ4)b*\mMO^<of_j4++<l:i_4eIt_2d-_]`*7@m%=h?WC-!o8P)jW1hB@ac\4CYf\J';R&nQO<>b+g0^`1Y/Vea`h35aCCH&k9XY:04?.KXCA4^H^RV7sdWM+b0Q6&2,Y'Qjd".<dGaEe7[>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+Mtce<s9I%TLe5B,=B'NuWgh@LsSjUKsG2nH).m4/&=\E>npB[.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]\[I2</a3_Cb0f"6+?\iOM>iEAK&4I*X7#S`r0%_^r[Hpf-B_SkhPH*3.%%.t=!<Y4pKq6&+cRX[\9du15F7'q3*L]j#qLG>\QiU.F.dE/>daUiHN2o*IajGj':,#HSW,0:%H"pN*A"nu8j\0]b\:cP_BeCYPE8BDbatf<9gQNrAfi=5EGZu.eCr[IKkfAoQZ87s.m1t\CEnd2Y-<g)XC#dA@[SsITHenjJR=E!4ID^HT@<R!\$=O1Em^bhfQuY&\ccTF;;qVC;1'qgE=)b0[R%q8]m9c>AWDE[`cMFDl2:-.W?Y44%1rc'Yc/Z`G-#<DW2?\69P+<<Z!Qq3Zi-7gN;3_f3&RrgtS.HrGR$g+1k"mmr'm!m+0-kG$KDGS;W\OEZ.aQgW)gkaZK6t/]6GE@mnABq<Pn`PF@95P1_>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_<HiI;f`?'[4LK`P5A&BT1l!eSk@X2CAJKfE7<ThK;@_EV<=9.[3!YoZLM8Pg]>]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/<IEZBQIZLj9@+kC5&T`l:PZBmOC]dS/nAr>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]<ln>,*<*/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)(\<O`QX9^[<oa]l0UE1&DA\IgQPo#ZAM)WN^=0@^+&(_FuB!E,98LZ`qe'-)Tg.)jmB<,7uA)#'64i714fE0o&UZ`JLO7gDJ+-6Q0'`Aou>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>E0um2do<eVot*k(IPPo<\U[fScY2Q(Je_siJ8PLk\5QN)3PG,f2kKh0:N3+M9uK+#sB^_dlhOWS:(Spn#IB~>endstream
|
||||
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
|
||||
[<f870e8545ad08e318cb8cc0470c788d5><f870e8545ad08e318cb8cc0470c788d5>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 16 0 R
|
||||
/Root 15 0 R
|
||||
/Size 27
|
||||
>>
|
||||
startxref
|
||||
21917
|
||||
%%EOF
|
||||
BIN
Les01-Introductie-AI/Les01-Slides.pdf
Normal file
BIN
Les01-Introductie-AI/Les01-Slides.pdf
Normal file
Binary file not shown.
BIN
Les01-Introductie-AI/Les01-Slides.pptx
Normal file
BIN
Les01-Introductie-AI/Les01-Slides.pptx
Normal file
Binary file not shown.
@@ -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?
|
||||
@@ -1,410 +0,0 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> 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@?rNDLtCrcfG<uqG$1,tX2^m?V;FU"-PU:[ePOiHJ^\rkCCP#8LaRdP<Z4c[j%"@8VWg",AQ_s+DX7Y?;/NTP-VQC#X!0rpc!71(YiDINCTojf!u;$sA!5/,~>endstream
|
||||
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\a<f%`B[s*rk=5>7bVo2`TIL0GLI!,"'Tg,KY[XN@#$hmVBE;rk??dhbcH[JoTlogp@9$bJO7!,tt&U:$ltAEJ/ls9VW=!%8_gF56fataBk0M"g?k<f(LA8Y$%jFIX7R-&EnmUFQm`1MdP(hs2c'pC$4mu"`5MPj9HA3&H_nZ`*J(X_[J0<TA^`GS;+50).hh+pbE;0IdlHZ*H64p_08]=)hDiK/@kLgJ7oR\\Q_9?jVk$jF+(i(,U(NqX4lOt(CR`r[ETLA[e%/=kL.>&`C_2ujXLE0dFDOYk,XMi<PSiH/:Hd*qFnIM<K+ASgoF(4E<u33S*W+-]Ue0Gr!a(8b#`\^h"pm7aKNe?\[:UG0o@("eiX*3d5G:t0DTFmE6ksRfA9NWWE![CBMtc-)u+?r.poG\?-CuWenoU"jgJ;6JPIdXQ<J.P@IOM%OUTD~>endstream
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 730
|
||||
>>
|
||||
stream
|
||||
Gatn$d;IYl'Sc(%MK@=q8i=';3!i[R[SE:B:><k7,];DqPsF=Ga3]!*^I(8[>@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%I<un^'j-S#!Lgh)F3)6ToBn!T-/2YH$sC'aJH39Wk_L!@Bn%_\ZLa`"#>j-+Rq**6C*IA"9-u'rk&3&Ea>HP5;cEkA&!Ym1?Lu6WZC5[BH+1#$,O*]aI3OmjhbF4cl]SBK(Ra];<DobLX6%hFS*?6\c[#p:Q0BO]$8LI3.X&s6Rj`1F#/AkUX?>C9V**$G1cjB\\jV9\`Y3B_B_<"f!VpTX]g&<N^\h$S_tS_><+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"<s)AkXGeVsm.lkLL["ToK(&ESZ8S^3-LeE_krm7JHjQ?NRhF2TaIg8rr,#t6S0=e&_[8\,@YT)K$"krt?mMF%"H,SnI-*%Q=NEP!dm"n=8+*2C=N*?$8?Co!gaJQ3F^?=c.p*P^]TC=5;V3_#9Rj>,$\^CN?'uXSrfV&B0e&9Nme!4EiD*bCRFs=t`oI%o<N/'H2O&bS[6>;B&Jok'Zk#"Y!teQJgmqjAD;m9oMR>,D-P!2E0aP0"T^PX+/VEth.p+<e6<+asV@r(H7?QBLNFc.(OA&EP[?X*]gt(='MC/+YjmXQYEC_+[q[*BlBZq%#N4,5'MgEQ9P:UgRafR?)\fsC@0Ip5/^n")7>eqC<>74Ldk"_PV/!uUcW4RZ5DAt'2@*?><Tt]U,!p8dJW02ih`AKPnC`T@K.=??FCRO1GP*<I5r*:cCmp`Cs?#T\-c)XgtH%O'('Y3m)'Vca0Fpdk2A-ERVPGsNnB])X`2GXUceWTY[E7cbeKUJfDI6PWtP2"?'G(bme9@(M^orQ:_7`jjDD<Bbsg`?-4,I'\:_4])@$DjdICW:CaqrPGj2\s_d,S%S[*YQ#in:C5E\N?FD>H]<\\Uk8ifi64Z9MKB07Y9:GI*g0-LBIlBm[A(S*h5)$_s@/#00ptUSO#"OpFO`cKKEbuUfSP8%>;9m%F<L=.^mIZ\8)+;h9!m:ja<sQMi(5VG9s3PU$(Roj7YX^EIbP2"a'k8J)(d#2T,:"B7a&Mo-OON`%qoe1!nK7:!0OW'SPm6P8T=k5Lc3m%CB"<YOIdGNQ&L<k)Q;Zs)iC@RQKOm$t`hGZFR[jpRZB@ahM)>N10~>endstream
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 236
|
||||
>>
|
||||
stream
|
||||
Gatmt:CDb>&B4,8.Gh<B2FghSknXs9?%TaYVpmEM2:Ld;GAi_W6jF5X0JNO`mkFs3K8+b+J[N=79-=`l""C3Wp=L4%'D0sW=p>[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<!jO5(]LH#XV_)NV>![kej&IW_AN;hhD"[%Nd\nP1b^5(U8MR>9R[kRa^diTjP8+Cj4K*:?kLr;R>)$:<b#bsZ/F&;'h0bHD)2@^Hkhk_rB>IRQK%e&"aI:X!JGm#oh&"f;F__;?G`hB8ZcY5>hJG"=<o"8)A^-)J2%!]?Dc[,M`_M64km[Xlng,DgAnu/M@VW5HN/V>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<iIYG(^4LXZAZW6$=(!4nC=m$.`<iE_1n5==l_%G9FN#24f/A2'A!8Ds]tcJ%.N+Nm>>-U"3X4^jbhIAJ_Es@=o;9agk9GgDV5X(A<S*P\%R':!8>AP<4Gc#"Hl6d2P/t/#>d<Pil\EKVi<jGcY>:,SHmLoQfS<G=5c7hqIEi/2e%Lrdp"=cR6<K&N/_'SsAD,!:;(-H(@=>[!Ccl]N8!WQN9[?s@1rH$i`?#NJ<2(00r5?5\Y#1o8)-;DO\<B*CB\]:eRG.<7!oq#[ason'^c1J2[i3MCDt$;*Vm.4.Nr%d@Dh=2:0Yl1hM`g8bJ)'UB(o5kHg#g*qV&^B6qYRIKj1kt?'V9fFk[hR0I(Y#!!H/fd$T[6&](Li"T3hcJgm+cB>"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#u0Fm<B8gFV&s=9!$%!l5\T5=[M[r<1o:'I\TE!gGbdP0\*/oPStsA][;0d)_7j#cQbeK]<%$B8;7+$NrGJ"^FlC+r50'7UPk.:"m1pKkKV$!@*j;`[V4GF!D(^@eb&,^k^B+36UJSm6Pb%aihj*=+!/b^r7W%AQ7SdL\`8%3MPGKkl6@P)[)+_1f;g0?_mT#8nGD[?298'#":_HQB2o5[o[UMZIjcOo:1>mYbHHtJ-<!*IHL;cEtpB<LGa7S8$4BMI&Cikl[DLIhq^<na-=p!qN:6"^/)5d@L?<AgE\$CC83otCKaWbVR%Ju"r;0Zm`h.LM``L)/7G8-fDQ+"\u!ia9(u[sO9_k6YY/P&0oc&@LZ=]d]fSqAgp&]a$HisP8Z:R<ENAu*>F#fQ#U].ZAC<T@b%Nc=+Q]CT&?Tj4W\cH#2&EGIgp9^`4lN/=](NEN&-`LjUmA>"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[<["<IF`*%q\8)7n*,!I'Qh6k%NmW>[=O%k&TUrVq!$o[A/OX!udDblEA>(_<r+PGc]1;UD32'?pUHp1;NgSo/_?5l#lN,1@:;q?'1oaJI524bLAe>=*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(p<U>fj)`kE((AjCJGO%t$MFK)7nHPYiMm*?("niWobgNIiN#GR"<%8\0&Q+F_YB^Hm[]6_PCu+?*,4Z.\'6Q3)$d<f3_%VZtL1cNso7)JIGm+CRLNS3['=@"TB&7kOF<c/#J$n>C4U\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,'<q_GfO70UhZ[!N016:mc6J*TmPN[O\\hJW[O4<n&BhChFtFip(D:^&rrO83YMB%>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\<YRPpM&BirZ9fW]1QW0/mA4WSj9G!]qUnC\&08'tq,#6i76XGM7464o%kX*N"f)f[?O=8VQf&9'$!thffFYEu1eQ(gG*iO.X@W`3<TTu.jZoRA%W3rKFs*$4C'p^Z2an2.,$!r<^="7Y)n6"Em,d"4$[S\Z=RnHq?squag2f`i/11(Re7<u\r4?W)M]Ho;phb]oWQ<g`Fk6;gl(lBRSk-tZ1='Pch<=^S!2>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<QRq$]#ZipV9>"LhbO./MZfYC30*[#;'?\p<M$I*)dj.@WhB1r2/K&,3!nRPYKSRbG(Egn$j)gNIC_ker,>u1CpqV>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<Vr;]HN*3i"+-j8<V"#U,Rg"DEUK2ZsF?EJku[6K2mRVLEa7BiYQke.P6Q)C9CjhjpBdc3+4HOT%2B!1qNQ5:8]EbSNO3a'elb?Hu?\T9*J,9>--Tj!c5[n\QOopcj/nYr&=L"<OBGM[4Q+3b:9F]0*nia_hud\ABO?d(h1uWEEX,?LnAq)-k[&#qptoZ@iV"P_u*Mr%#u5)2<A13Q/b.I1AS8KMO`D#>*("#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<iRH^[a1O_09:nc^P>>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<Z`u'e;]D+NU$4-O)a?#1fdX9pY?HWOdScE@#,D1ETOcrVYA.&C21ges+a#0shk<4u]J[4UpQar9c#"T@4q@$^1B1s25MHd5^_ri"2+>?D`m:OM"?'@\+VoH*hQnVo,@_29@C&3:=B$V?ge%"m.[.j>V"W-"aWbr/qP>tA&&^@Gm$QQpGB+W0e>CbNOuhnb6X)##($$\0]!o<bB;:?f-eB$C/tlHEpe'/bge;\\Aj'Rtr?K;=3<SE4o*a6le9GHg.2TD?PKUNgTXpZ3p4VDSS^MuY;iAU.GK7WF4oQ7,++$[e0>BU@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)WCM<Y0J$MV%.03$`njAkW^q65<_jGZ00tci^8#5>EYc\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'--1J<M-@4$qu>riL!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/!!#4pLm<M`]BhTp5]BL'MJ#)sF?M6W(R+ifZ/*7P)9i(j-F[6XSE=l1U<5?$:XU/U[qf0p^A3$.p*:L5E>f_,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`<fdVVQQ8.<P.NG(0p`pRiC9+H6uJ;4@H\'6(''\)@)GcQbm8[HE)DHaIAlaZ"@i+\hnZgV0/j#Fa3oQLB,T5fV+<@3lo9!`PoUQ+Af("\j"HT\dO\f\E./:Fm9V>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.,`dAs<J*[gm#GP$\u8]^%]j"M=lhUR"Bq<"07AROJP2;e!.i-YT=(CV.9aT@i>3\Yga5>R==4(@#<a#@9^oS,%:0[h(F%/8`<\<1X"KtA#V]\\!Y_1>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<BYU!lhGYlM]jO-]jq_d,EYtfaT9=nPA2=Il1'4F=a#TI]m2,p5$Tt'pg*[F!X-]6d]3=hCo,Qj)I^-*Ft[iNJ.!VT/rZMOaGcg4/AY0[C@;[NN:YLL4,!HS[7s**6k\-'ahm>+rme<U9736S?tMOBYa3&\60)[\];['^Jc^)s1!/M%&5#6`?H:'%j'+!?k\FZVR0<(\&P/$@o2/[)6m;:L)LQCX@1Bf7_I4N`p"m5b9StO"pZH,[#!ui)$-Iq+q@Ro,9XT1EY2rn`[V<_Tp&<3:/0V^iWVP4YJ[r9_7dZ:m/0IYAC:b&+t-sOHPr8.UuV/^=*(k/nl"VT0O2fLJE,*5I(%h!f("D`a6h:7aK0.-N:pOdCUqA0D$Xm&jB7]s+a'=7cmeP^C9?;P3h3@DXWC'J<WU5)iX9kX0AEmi?UA7L_5t`;;@2]:YQbs\Ct"%$d*VLNBQtZ$@4?ik"slTgP;j8>"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]Ca<i`^9PLMtQPr\MNTqrHNroO-/GFc&##&-<mHn,XQupTe/DZqHtkYG2;GkYt\8kIF=-`Qo,VcKI]?B.hbe?r?3j`X)e>krOF(<g;94Z%%'#@1B@\cfA8u`9?N(b66If*NXiLN(&_#a(^5"G<;?[EH7?(XK7&0YWVC<F>p7=gIDdPRmt&/<tY.mq5]O3).@6^E;V8"8Yf=6\rQsW\bHjj:"/G0%V1nu,fdTN3rI.MB,9U6W(%3-l.g_G&\E>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;',<K*Xfp`r4@O3P#iG,FLGcWY1qAFDd]heK<Ic^M>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(<A-c*^GFl_iPTbG/4?j#W+CNn2AmRRP@9OLJ.e^uFH!$\qNdsPJ,Q9ZR_K,e83!uod9gK$Pur(E^X"(>_Rk,K;Q9o@J!X>`i2`o$Z8l/\*-,sD("bWsq>ZsC#7:!-$gBXb[QQ,\KqjWZ,l<(4f<P#1mW[n/=(B<)'a-+g9i,8HR3.e+p"Ya$2fnA'"'q4F2o]]I3'&-?D5nioX-]qu=Q?Y?9,!o?&)E4aUu4\/6VS@S:JZf6)S@rCK,ru;@;VG"^VWI.2RZOJWJu'\<3SoP=Sc\LeT2;7ipq5'4)jqYhcBE>$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(=QGFYJ<U+>FNbWY1E!+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%<m_edO>PND-r!%h`gflWf']XcMoH3+e)oXuqV8kP@L>_E;iDQDuONArg.bE&<B_ZbA31!OCBr_'^S%]n>Gd5+hf5O19Z%>VJ=/aD%KD2<VQt^75KL3uIGr:(^+O>+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[d6DV<?:<Mjh?_(I8PViUi@[]%<AJH.@3`c#tNmi)@N@4u^?JSM-+8jd\%02mk3P#i<0EbOGW[hl?)Lb+Y81,Epq^-03p]jW5PKq,<\5>3Fq6>L[dpeA1]X+Dp%]r9?lf4RQ_'-uSp)%Rp<)<tI+RgbkCM,^Okd*WuC(Te,e[_S,ngR?Y`1^l>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]<B)nFD?'pZK=^N6LU4u'jh++1G.Mu~>endstream
|
||||
endobj
|
||||
43 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1256
|
||||
>>
|
||||
stream
|
||||
Gatm;gN)%,&:O:SD"CQ$P<oCsca967R\Y'RESOa>6gHp$e4P<=Ji!`_n_[tF9!hck%Q!827Ze)<SAh(P,mTK;]_TinJ<r'Z^>Zj2ba-$H(;T8g]Uk[!TC@8>OKahE;$q<eR3-dG`mJ?5,a!mM?p_dtaFU03%Ho]^F[Gi`h)'66S>Hi?4?%s*DM$!R6J'b/3)`q/69$X,>Bh<6/iT`$_X%VA5<'4(dd'(>6$Nfk$Qd`!`Zm-3!Wkc>PNWL<TERYe]p<F9jC<`k=OoO>WmGde/1M%95U^["/\7+l<F?CoK9q&N)q<1=JFl!j6r='WrW"!^!T2*fr0PBVQ";raQ5?jhTFY(fnh/ksT"3SnD\;UDLC%U4rr-@2^Iu8mU1rTB0#7\I8\'ENfu"7HQ-#mkLH]J,jF1j$g*3lRSae^D5h,0Pm4';2'FmZ%PR-DA<0"YKLlsCKYuPthitu)9cp'/>T>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[+Y<Q!U:%jC'$^TkU-s4?,.j>c6SOn19@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<cp(uGg7,Os!*'KD*^h5?44VJ'HG$5=lt][)Z>!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\;YL29<t]$V:]XD$gZcIsTpleON1#eUm:aL,OUL-<_n@%WJPRArpmY=bY6N@#_ghnE(]fQ-#R6(RHLTk=#Ab+mAR<USP!)?7k(Te\BH975Kl$?b[(:i3E,0f=!XO2JCL35O5oP@l"DJQ+nFhIILc3[kkROu"LF&CG;HCjnHjalYEqTbC;]%W?qu99\gFu%c(']g'/>g$#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*&$Bu<H<R$c(Wu(7;/Tb`[X%/`D0N<+XS4g$h7`qsS7?aq^^&E/icGK!jkp4d"$^bWc6Z"L8iJ_PYR>7G):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;_!*2n<lRVKGAQU$Md"r1W,A:H&F/5p&^:dbs`["ankh9idH=S%`X;L$]uO=5Scdm47AJX8m8>h7+\[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"<jFpGD2[@;#-=Bp,Ee)&Rr61%dC#N%.0_:Ga%@WC_aX5pZ:D^*+*F"rCokS4F:I3AHl=*UL>QEQ*L!/5Tm)W_K<mTA+],#N8,VQ@'lbZmU%0ILi-b1!KgDe_</-)4a1+cJpZ`d0q!9XH\8ooXG&__<&cbT,1Fe4"@5m5fH1`CC6A><BcW*=3LpAFq&8*YPXRbphgLJ"+75Lf0ITC_0fD#G7SWl)#8dJ;J]m.)GaY>jQ2m`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+<SfcEf)bdB\&2]8qW6Yk%km??`mVfKBWO$8UUd`AfX5+uV^tR$b15:u^=UC;2&_<VS;)#SrkD:?\_.37f$XQaocu,W)NTW!D\MnkdSY&2S6k0BR,0la2Klo^nB^^1X&/@**`[Rq*^Otq4FV<%VDhWd>qO.7XK6$9DtGR%WQHS\NJ\?#c!UVhb7\Be6R?7A4VRXa3?J?$TjFWpTl\G#6/D/;$!"g;e\@40oT<FIZ24NKKNPd[]&gI+21l8'D<XG+Zj(s_&0C`WCP&BZ~>endstream
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 (
|
||||
<html lang="nl">
|
||||
<body className="bg-gray-50 min-h-screen flex flex-col">
|
||||
<nav className="bg-white shadow-sm border-b border-gray-200">
|
||||
<div className="container mx-auto px-4 py-4 flex items-center gap-8">
|
||||
<Link href="/"
|
||||
className="text-2xl font-bold text-purple-600 hover:text-purple-700 transition-colors">
|
||||
QuickPoll
|
||||
</Link>
|
||||
<Link href="/"
|
||||
className="text-gray-700 hover:text-purple-600 transition-colors font-medium">
|
||||
Home
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
<main className="flex-1">{children}</main>
|
||||
<footer className="bg-white border-t border-gray-200 mt-12">
|
||||
<div className="container mx-auto px-4 py-6 text-center text-gray-600 text-sm">
|
||||
© 2026 QuickPoll — Built with Next.js 15
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- **`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 `<a>`. 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 (
|
||||
<div className="container mx-auto py-12 px-4">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900">QuickPoll</h1>
|
||||
<p className="text-gray-600 mt-2">Kies een poll en stem af in een oogwenk</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{polls.map((poll) => {
|
||||
const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0);
|
||||
return (
|
||||
<Link key={poll.id} href={`/poll/${poll.id}`}>
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-6
|
||||
hover:shadow-lg transition-shadow cursor-pointer h-full">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
{poll.question}
|
||||
</h2>
|
||||
<div className="flex items-center justify-between text-sm text-gray-600">
|
||||
<span>{poll.options.length} opties</span>
|
||||
<span>{totalVotes} stemmen</span>
|
||||
</div>
|
||||
<div className="mt-4 text-purple-600 font-semibold text-sm">
|
||||
Stemmen →
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- **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<NextResponse> {
|
||||
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<Metadata> {
|
||||
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 (
|
||||
<div className="max-w-2xl mx-auto py-12 px-4">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
||||
{poll.question}
|
||||
</h1>
|
||||
<VoteForm poll={poll} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- **`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<number | null>(null);
|
||||
const [hasVoted, setHasVoted] = useState<boolean>(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [currentPoll, setCurrentPoll] = useState<Poll>(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<void> {
|
||||
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 (
|
||||
<div className="space-y-3">
|
||||
{currentPoll.options.map((option, index) => {
|
||||
const percentage = getPercentage(currentPoll.votes[index]);
|
||||
const isSelected = selectedOption === index;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => !hasVoted && setSelectedOption(index)}
|
||||
disabled={hasVoted}
|
||||
className={`w-full text-left p-4 rounded-lg border-2 transition-all
|
||||
relative overflow-hidden ${
|
||||
hasVoted
|
||||
? "border-gray-200 cursor-default"
|
||||
: isSelected
|
||||
? "border-purple-500 bg-purple-50"
|
||||
: "border-gray-200 hover:border-purple-300 cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{hasVoted && (
|
||||
<div
|
||||
className="absolute inset-0 bg-purple-100 transition-all duration-500"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
)}
|
||||
<div className="relative flex justify-between items-center">
|
||||
<span className="font-medium">{option}</span>
|
||||
{hasVoted && (
|
||||
<span className="text-sm font-semibold text-purple-700">
|
||||
{percentage}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{!hasVoted && (
|
||||
<button
|
||||
onClick={handleVote}
|
||||
disabled={selectedOption === null || isSubmitting}
|
||||
className="w-full bg-purple-600 text-white py-3 rounded-lg font-medium
|
||||
hover:bg-purple-700 disabled:bg-gray-300 transition-colors mt-4"
|
||||
>
|
||||
{isSubmitting ? "Bezig met stemmen..." : "Stem!"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{hasVoted && (
|
||||
<p className="text-center text-green-600 font-medium mt-4">
|
||||
Bedankt voor je stem! Totaal: {totalVotes} stemmen
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- **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 (
|
||||
<div className="container mx-auto py-12 px-4 space-y-4">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/3 mb-2" />
|
||||
<div className="h-4 bg-gray-200 rounded w-1/2 mb-8" />
|
||||
</div>
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="animate-pulse bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="h-5 bg-gray-200 rounded w-3/4 mb-3" />
|
||||
<div className="h-4 bg-gray-200 rounded w-1/4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**`src/app/error.tsx`** *(let op: `"use client"` verplicht!)*:
|
||||
```tsx
|
||||
"use client";
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return (
|
||||
<div className="text-center py-16">
|
||||
<h2 className="text-2xl font-bold text-red-600 mb-4">Er ging iets mis!</h2>
|
||||
<p className="text-gray-600 mb-6">{error.message}</p>
|
||||
<button onClick={() => reset()}
|
||||
className="bg-purple-600 text-white px-6 py-3 rounded-lg hover:bg-purple-700">
|
||||
Probeer opnieuw
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**`src/app/not-found.tsx`:**
|
||||
```tsx
|
||||
import Link from "next/link";
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="text-center py-16">
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">404</h2>
|
||||
<p className="text-gray-600 mb-6">Deze pagina bestaat niet.</p>
|
||||
<Link href="/" className="bg-purple-600 text-white px-6 py-3 rounded-lg
|
||||
hover:bg-purple-700 inline-block">Terug naar home</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- **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!"
|
||||
Binary file not shown.
@@ -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)
|
||||
@@ -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!
|
||||
@@ -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
|
||||
@@ -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! 🎉
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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 |
|
||||
@@ -1,334 +0,0 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> 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#^n<b"FcUjU0hI$2WaC.p>6e-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>cA49<luXgmd]>34Is01^AsKPDddoLKFd@#KOsJfa,sPVn#T9Y'>+YIVt**IWL+D@<T"MlOl?"Q7]G'?/ar3ND%Sl<JCJ@MM4]90\XgMMm(fS$$hXZDRtDdHiq+QWP-7Lm!Qfs&eJP9'-*%i4pFElF-'idc!U_1.Ulp:r6l9"8cuNU4Tdl*"t'$QpuDmZnuBNBaV>FePP):%WTph9"UV0XZ8n2CX8en-3b3Hj,W;,g$/LS3i<n>m$!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)!Y<!_k;Xee)WrscMMG@m1g.T!9l7kg=C<$jI`RNn]Y^]OW=hTCaC]NV0<l+X,(4=4:$'^a1`U)X+.TfcrQ'/s%,b;6rJB?X3-O4)FMO.C"j/"Jq2mR-9\1cUKbDFMFQ=R'TJ-p+]Ko4;F`N5@SQ.,fTZY9p%#s34<<K0nF8M1F5n4kY35?T4XakB5,*dLa75q[;&_A2qoSYaFKG&=eokp:1Oero/U2/6"0BW"]IKW.nVdrLuc?5D8mf2Q:&R[_e0$#H%1g%6ZjI809+&K5V62EcT_Z512SXG0`OlIEfQ#dS.EUmEs@*fd=1_*jhF7(-?i^u<A3'9HPW/pP2^3bQ#Z.pCOoBVA-^+MH.dpP6"a`Em5l9iE1[W,"*!DHo'].?cU3600dN%86g:RjLFkW!3*>1N'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<X#3h9M*I%]t7@mLbA#01E]8R3,Ir8Un@V4t+hYhchf./XS1p52$$CYYW'I9slQE5l=&gAtR$$5Lj+GC0\F>"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<Y,1*?dZ<6TY2F#E`Y#e1b&W3Zj_`eS.R_*-;t5>;;.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\K<K$;'g!&m*`W#]M_uoY1^`P6s^0hiIB9Y;\uO6^iDhCS9Dr$&bidbY0&>Z;1*i,T\uGP9N#OrY@r.,O69p.F#4Fh8,_.;k7?bdhY>R_R(F[57l>t0.D6!%U'f_D6sq[[D=NpQ+4<c)]MX_0o[OdB8Ys&\;7J3]8#EnS5bR)VBq\V=/NN\#J#M!ULpD!C,`?V2qf4;[pnp$^hSjh8Idr9=B5M`1LGHmRK3E35oVJVdLOf-fSGM1`7H_,8/O!aFi`3?aQ.M]d9pCT8h&8@^VdL^VaKj.[CRF0i$enR:=[5Be=7<A'5?JPC'3.88@3b-a4-;Vgg5J)o)%*Y2Ks[?+c?@@k_qo%%t6<\;PG.aP#<`fL8%EQ%q+)f^+a+5=#DR+f<SepT8R?>\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<Zj^cBrP!0?aqnbiKYZLXHLn5AD&'MN38"[5N&hT.GW<FfA'_>"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<A'Fq\"P;b5<W,-YH3KKNRr[iS'<Q.lS#bJ2^!V#5JJYfK)@tk7IgE+$J)5?>#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%R5<oG#arOViajLftu>nODl9VbK'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(?<L;i`ZbaROp<YnF2u0(qQ`DF]ZVnSWeYhQ^ROTE5>0i>Pa*,q1I?U_Lqhj!HE#7lC*j+hS[jtp;kl9fIIT?dMEFCq)@0u7Kk33ZuHQZPVpM.V#8\[DQqS2ZL&8fqcd/---DT)1[U<AJ,UU2>u)*`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<PrF4IRc,o7f66<7b"6kkeJeu4`PWYU.(5DIZ6,a<U(gRfjXX;?ogZ^`Bhjf-i=4gFlE57?Rf70>/&7K1:<K&RC^SM:Q*hd]WH)>&,E?d?s:"?>H6QjOr2pk0b1\L)Z1Un%I][e0*miW[:fM06bf2al`kp]<bGZ.jjMLOi%7^uoo:#^OBt_\CngZd_Pk/9Uo?A:b0G([;O6HO@]--"+8.L&/I9#K%7WTF'O*%.9YiG!bnSs#X$oU`e4T!A[m[9O?B<$_9-W4.X2!ah?Pq<6-QkQ**/#Z&c?lEr:pl3W<5=VUpqUoQhLV?,K[X'icfd]3Wk`J4?:9*:0Ne#cJ;[.&[=\1956sRuIFB0`liMV,drYod!%eIJ57I%n*5[.u_AH]=mkQli5j)J_>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<TtE,j8LTf?`nN><+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^tp<Ch:5cTC4WSd;-i^hdZ->uD5>7DBCNKuFUr>iUaWe%>inou>dUai@PitA=job%KikuTCp@bb\+aGJ)\s6.;Kc(.Ck>KE,cb[X$c0,c<tCkGbHqaGimF\DR5At;G_>FiE*V^mG:jB>n>WL]+;h0L;V<rJDd&qkn`$dbD)$XCo6UR*r7LIm&P-<TMm!X\@'91=&[0^$L]gSRkGF&eKLCZo[2tg1ha6W>aej[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)<I*i:DJASq1ATOV9I:"O(=P&5aE1[[9d=(gL7T*`,UT.RT&6.cC@8RM'%7^pqt."D7J6E1@74RApTMKG@t%jRFh_W1cTsUX4CJYiJ\f_>FN"05GC=e+FPA2sfKb"6%cQQsZ1tfWlF<dgX%e:niK;A-9O82rZ#7$UL00pg]Ftmkj0m<qi0>0s(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@rS<lY<"1V0<g&p3X;B'>9gNVf2;?.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<p*>=: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<fPPM&u)0XrZ]*bMh]5J`SKf:5BTQ\(cMtkTe!`_R7eP7H7#?NmDi'$:$n[j-\Yub:+bVm\J&Mj#I<t5Kf"`K'O?2nsSLS):1f<nC4d7X-f@D[r?jPZ!p@qocJeV[s2ZRjsXrQ2Aslq`.;t66>+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\<m"rkHr;IggEb8]'8^(fG!mKc>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$(]"<e#0lJuBu]/?SN4&JD?>4]YC+nJ=X22X7Fnj?eNBRHWI"/r;-=nZPfW3#.%4C,5i!LEWaa-r8cg54[mN57;U5G"9TR"&W2!#Vc?G:;n6eQVp"!0]9bc*(9<WFHM1rbC5L-T9]99D"6dFf8U](B.AGl:ngJid6Np=^#jIP]]Y!J\SZ=-Ved8_X?aQo8s^%IGM`?9fq>`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;<Fr-S0ob?WZ/lI#0%J;A7LDJo4Kqp?$)55bl2j3>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_<S'ZC]NZ.%0W1aR"MUe/Ah^>teI%TX2U@oM-tVS@RM2e,1)1oK,FBtf_l)inS8$4aeLqAi?45)+$$8qA`'Dh5L<Kl@%jYk>=k%FSb)$WoZfk6?SD?@[>M7R^La70<gn5e0iX4CpdW>`MVcnT<epgd1fo019iI_1QtpH4/\">G9R#0[%r;h^!r1.$huNX<<Fq6HtfAM$Y"&]2/=bG]\!(WP>^`#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(JdKZ<JVME?A*d'(,;UU&Z5s0R%JHcTmS6>d/9GA(ni:C@=;*q6X/UEe&0r%Bg`2>K\qrmD$k6B@30M@i%U<HWI[abE66eO_cdhF$>2SVQ?ki+#I:#'EAqLec=\;.N/7Tl>Mk&#WK1#Lt^N!)X:jFPX`IFJ]#2lR[s(K!!@rR;6p(H)4OR/*5i(4!<!BhM,MID>]%*;9-rnm=r2Iu]df&mt/jBb<RkGlXFBLS%e<1RC!I9NMH=8!piO^hM(UCX^Q9f0DI5$tbHQ!(U)J\kN3cX8io46\$C5kW-ZYEi44[mmMS*H!*c,I4]!"CWB%]4]mi><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\hi<Z/;c1C04>R4N@jg)cj$PBZ<q#C@U_<]-X-'J)%G_cJeXYF$2Ya#\,qH$5^]4:)A?>GSQoIg\Jq%(uB0@ZRB;<Kn%cojVC<Q[KCJMhZo^Q)83>^/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$^'<Cd/HR,"@a13o@OTOKYWhm0o6/<he-moSg8VQhSJ[^L@8&#T@L=UP&=5B@86^9:k.%>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+&_Ycc<s89&Ua0JkmK%uYQ]o/kXTd9h$1j8`[d2$e12eM)[5S=_dMQC:_<347cE@fO5]<fk.#p7@5PL)l5fU'-R2W]UO+HMUtb,rnG2*1/GK;R,q@T+6D!;2(1=J,hSnTqkksMf,P(d0I;k&Tg0HDUZ$q;mfZX)Nf@`th"gK1ooEgP.o-jef$1I+88BIdnTu[cNrg47Ji$2A[!b_WC66eMl5(e)$6o5%WS=A4G&r8Q=.Lp$A9"g>A?BH''TsF09h&eo+<G+pe-8dLI^*R^Y#a[>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<YIu/%l2,QTkmDo2TR%IXqjDVHo>]3%/n_"8D(>YY!0;fKA%_57%#=[c4:4dEOB*K[UO)[V"-gl>tCbf1[O'YPHT%j8BTU\_jmUaOY:pIni6<0FQ#!%q<RL,jnFb/=='2Uq-PSMjWb[$Y`if<^XHl4,uL)7q(*ZJ733sS($Cg/8#mfB32S+kjPP*r=`u5,'5:Q#j=(VnI3gNof@h$`auEl9T?)tLkBu;k/kD8C<KGd%RVb+O*^/_c6W9i%[Sp&FCpnqmp9'-]WjWdIL5RTa_-6gS_4Ut6If\uB>k7~>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<A$N:mM^:.d`1N5n3GPW#"!@>)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$<gnF'<pj$^[/k'Ihe=J-ROJ^CU$_R4nbDWk$T%IN7J_Sk$U&_s@`eke$2)=HI1i7krkNdhQY4>-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"I<DJ5#8k#G1*\A1REifFO:HFaUfcC_&qS'3T1IX;uEnPU`o5`X:hQc9[oK@!$$YkAUkcO*Q`m`]7[ha8[jD50)#P1$0ZT`,ktmQ>2YYcG_E$aTNg2`#j+u<i8HbS<Pi&_?JMPRca`9J%:*[/lgFCP6Q8Vd$lsu=0FmbE]K('.u=L-W#XiB$$(7j0i-7CE<"h4cG-,r&gPt!I(3>'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<!L`'qLr._><<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'g<deh@7,]-J^a4<+Khk"#Kt3=YB?7iNGkCa>i3H,)>JG4o0`.c4dX$.$X3Q3jt"oC~>endstream
|
||||
endobj
|
||||
33 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 675
|
||||
>>
|
||||
stream
|
||||
Gatm8?#uH"'Rf.GSB:C(drAfI7@;XiUoBp/fT0"/:HX<q)QBrE^c3\$J+)Jdj$u/-9p+t_m_.eT]6XsB8KV8e%o_C1JC3E8(4n9N_]/f-HjI)d)K.B:Mn-R`dt@$Cm^k[5d5LTDca8*?`buqV-Eu!en!Y9C_-tQ*22/J?hMrtN7qLVq7j`4qY$D\r;;tfGga5K>Kp%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'\l6HsfBP<sbUspa+tL%DCMT4#OddS_/KEJ;+`*2GO*GNn&e$U1^B$$T<(m~>endstream
|
||||
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<sZso?uuLGEtuf(pj+K[VGi;JjKbUe>&E@lk@Q0`LZ8QuT?)4&"mo9EW`L#Y?UUD]-:MDX+d(bJT=-Ij!+3Nn8snZ%9-aVb5cu:<B^q,&\!6QXc_fC$MqC'Ra)(HWGPq?!$>IC`L%IE<f4i(hLtFM^46kXBg;Lg=cn[Zo;n+g=N_;>h*D7X-b`HR+ml@%I-\?pd#)LB:SWq:>#S<k8_HPQr[_X)?BoL,4Iq^;f7!1RuYL*Vm:q_g5,[c-<,.u@UfA?R@RAKXdi8)-5_q<oDHU%6Y5(eDEC:o6H%t$pk0'KtYbMu:;XfFRo!Er\pB!2CNNBKtWS^AA:rZEe]*ubaDB%piPr1"4FYhZ<"XN*eMm3Vk>M19o=*jSff]F]GKZb)l_C.pur#"o;]<S`D.hLl2rI2YJs'BW48]He7TY"(T^P/c&-q/Lij3>>`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+<pD=#P2Z,\/W<2=89(Qf]0rQcTE$PD$(eiblH&*)p2be#XY/5kb^8Gcu(-oT`S:dCFkae]r*<T'NuAXd4\#ng0$nL/R]+d,8gYsHbg?A"m"`=1E<R,bM3LKqet-8Bk!-Q]H;oe[U&2XF`#?eFM3'5Z"*?/@%lrGMuWb3dLJ/C/a<^NBk8Kc;I?q&W6P/Cj8X<n,3S=Ns0Hhr8qT%R.FD:,F7\ps7(fNP]K?ko>erE9r4&Yc,SQH22M0GKuX]\f,CR2"8uleCe)!$`D<YNHo)H;:01jPYs0IPShS2HM$I.AW1Gh53'!!)qu'u&3Qh2C,+s`@T$5$"-6IRqGb$l#a%1VR/7M9Bqp"NcJijXk+df3-R*)a`5%PLV:ij+qYSHp,RJkReI+tm+;%`h#Z)St+/(s%DYlLE:s1a-.F;DuK`X*sIa/XR8-#ep1%.\Bprq\.;h5QgBNo[u+CRm=2]j!j%6"V4MUY&S<CUlY]%=?")%<,Bk1oDmp%$;VW*29#d@@crksQV9E:l@a*QM]=KK?Z$G2+L_k''gON>o]:("+>/::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[<?;GIES'`3e6(c"tBiN'%'%j/^@2aQfD]K#]BLXrG8Y'"JZWd":;P7EZl%dV@I0AYYKp>(n9@mb$>r)b<n)[#J>G<.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"<FQtcW*0X=OUfgW5Wm8bYGB'62CSWVm+R%^Ud8gF\TZW^`&i-5NcV6B)-Nc1TqiA[tom>YA)dA*>Y&K(>6fbi,Xe#4u-N?V*r!n&m+(h^(krhGm/QAQ-AS0KMn*#iV9De-NCB7Koj>WRq(_EY8=bThsms8Yh@(m2[";SlYY8_s'5<aDg25rn1.IZ_e-E:CcSo$4&D*bnmt609aU$h3no#e@/k?3Ishb?O_SeQ!PMQPoL?Ad*4euI(4(h\6SLGRl7pH:OWHLa2p1Fr.,^(H\42,EQ`1i#k[>qT]i0%3KqPl-(2>9tLL)ellVMTA9'@]Zf8[u]1Q,/k?9ID].ZX3^[k?0DRl`0=9o.S<e)!t1_Y.6+cQ""Q;2D,u.5[dDL>R'/Tfi3%AsI[rU`FHaR-44S(-<K(B&oIBjA0\MP3I)1'6bSCV98U53mW&A9;^j>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'REZVcM<nW)k&`>seD"@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,"f</:Z:DU83\WBT7!:c5"(jm[U&iBH9n&cc,[hi*!9^ZIX1gQR">Y3B8=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%<l4W&!gF,hB`Dn@%4S,Skn[VVbA"m\9S"E^o*VMF,p);&gGaS3%'`%$/+EP7T%qb,]ljPl@QgdPgUtiZTLUXuH(66UV^,2=5Af-Dt>Dn8[MYn2uk\hnmGDU6(sH&5eia'Coot<l/YUU]1>?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
|
||||
@@ -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 <PollItem poll={poll} onOptionClick={onVote} />;
|
||||
}
|
||||
```
|
||||
|
||||
> **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 (
|
||||
<div className="w-full p-4">
|
||||
<h2 className="text-2xl font-bold mb-4">{poll.question}</h2>
|
||||
<VoteForm poll={poll} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> **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 (
|
||||
<div
|
||||
onClick={() => onClick?.(option)}
|
||||
className="relative my-2 p-3 border rounded cursor-pointer hover:bg-gray-50 overflow-hidden"
|
||||
>
|
||||
{/* Achtergrond bar */}
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full bg-blue-100 transition-all duration-300"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
{/* Tekst bovenop de bar */}
|
||||
<div className="relative flex justify-between">
|
||||
<span>{option}</span>
|
||||
<span className="text-gray-500">{votes} ({percentage}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PollItem = ({ poll, onOptionClick }: PollItemProps) => {
|
||||
const totalVotes = poll.votes.reduce((sum, v) => sum + v, 0);
|
||||
|
||||
return (
|
||||
<section className="w-full my-6">
|
||||
<h2 className="text-xl font-bold mb-2">{poll.question}</h2>
|
||||
<p className="text-sm text-gray-500 mb-3">{totalVotes} stemmen</p>
|
||||
{poll.options.map((option, index) => {
|
||||
const votes = poll.votes[index];
|
||||
const percentage = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
|
||||
return (
|
||||
<PollItemOption
|
||||
key={option}
|
||||
option={option}
|
||||
votes={votes}
|
||||
percentage={percentage}
|
||||
onClick={onOptionClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **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<Poll[]> {
|
||||
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<Poll | null> {
|
||||
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<Option | null> {
|
||||
// Eerst de huidige votes ophalen
|
||||
const { data: option, error: fetchError } = await supabase
|
||||
.from("options")
|
||||
.select("votes")
|
||||
.eq("id", optionId)
|
||||
.single();
|
||||
|
||||
if (fetchError || !option) {
|
||||
console.error("Error fetching option:", fetchError);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Dan de votes met 1 verhogen
|
||||
const { data: updated, error: updateError } = await supabase
|
||||
.from("options")
|
||||
.update({ votes: option.votes + 1 })
|
||||
.eq("id", optionId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (updateError) {
|
||||
console.error("Error updating votes:", updateError);
|
||||
return null;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "Laten we dit stap voor stap doorlopen:"
|
||||
>
|
||||
> - **`getPolls()`**: We doen `supabase.from("polls").select("*, options(*)")`. Die `options(*)` is de magie — Supabase haalt automatisch de gerelateerde opties op via de foreign key. Alsof je een JOIN doet, maar dan makkelijker.
|
||||
> - **`getPollById()`**: Zelfde, maar met `.eq("id", id)` filteren we op één poll. `.single()` zegt: ik verwacht precies 1 resultaat.
|
||||
> - **`votePoll()`**: We werken nu met een `optionId` (uuid) in plaats van een index. We halen eerst de huidige votes op, verhogen met 1, en slaan op.
|
||||
|
||||
---
|
||||
|
||||
## Stap 3.6 — Homepage aanpassen
|
||||
|
||||
> **Vertel:** "Onze `getPolls()` is nu async — die returned een Promise. We moeten de homepage aanpassen."
|
||||
|
||||
Open `app/page.tsx` en vervang de inhoud:
|
||||
|
||||
```typescript
|
||||
import { getPolls } from "@/lib/data";
|
||||
import { PollItem } from "@/components/PollItem";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
const polls = await getPolls();
|
||||
|
||||
return (
|
||||
<div className="w-full p-4">
|
||||
<h2 className="text-2xl font-bold mb-4">Onze polls</h2>
|
||||
{polls.map((poll) => (
|
||||
<Link key={poll.id} href={`/poll/${poll.id}`}>
|
||||
<PollItem poll={poll} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "Let op: de functie is nu `async` en we doen `await getPolls()`. Dit kan omdat dit een Server Component is — die draait op de server en mag async zijn. We wrappen elke poll ook in een Link zodat je erop kunt klikken om naar de detail pagina te gaan."
|
||||
|
||||
---
|
||||
|
||||
## Stap 3.7 — PollItem aanpassen voor nieuwe types
|
||||
|
||||
> **Vertel:** "Onze PollItem moet nu werken met de Option type in plaats van een platte array. We passen het component aan."
|
||||
|
||||
Open `components/PollItem.tsx` en vervang de inhoud:
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { Poll, Option } from "@/types"
|
||||
|
||||
type PollItemProps = {
|
||||
poll: Poll
|
||||
onOptionClick?: (option: Option) => void
|
||||
}
|
||||
|
||||
type PollItemOptionProps = {
|
||||
option: Option
|
||||
percentage: number
|
||||
onClick?: (option: Option) => void
|
||||
}
|
||||
|
||||
export const PollItemOption = ({ option, percentage, onClick }: PollItemOptionProps) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onClick?.(option)}
|
||||
className="relative my-2 p-3 border rounded cursor-pointer hover:bg-gray-50 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full bg-blue-100 transition-all duration-300"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
<div className="relative flex justify-between">
|
||||
<span>{option.text}</span>
|
||||
<span className="text-gray-500">{option.votes} ({percentage}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PollItem = ({ poll, onOptionClick }: PollItemProps) => {
|
||||
const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0);
|
||||
|
||||
return (
|
||||
<section className="w-full my-6">
|
||||
<h2 className="text-xl font-bold mb-2">{poll.question}</h2>
|
||||
<p className="text-sm text-gray-500 mb-3">{totalVotes} stemmen</p>
|
||||
{poll.options.map((option) => {
|
||||
const percentage = totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0;
|
||||
return (
|
||||
<PollItemOption
|
||||
key={option.id}
|
||||
option={option}
|
||||
percentage={percentage}
|
||||
onClick={onOptionClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "Het verschil: we gebruiken nu `option.text` en `option.votes` in plaats van de platte arrays. En de key is nu `option.id` — dat is altijd uniek."
|
||||
|
||||
---
|
||||
|
||||
## Stap 3.8 — VoteForm + detail pagina aanpassen
|
||||
|
||||
> **Vertel:** "We hebben in Deel 1 het Server Component + VoteForm patroon opgezet. De pagina is een Server Component die data ophaalt, de VoteForm is een Client Component voor interactie. Dat patroon hoeven we niet te veranderen — we passen alleen de VoteForm aan om met option ID's te werken."
|
||||
|
||||
Open `components/VoteForm.tsx` en vervang de inhoud:
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { Poll, Option } 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: Option) => {
|
||||
const response = await fetch(`/api/polls/${poll.id}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ optionId: option.id }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const updatedPoll = await response.json();
|
||||
setPoll(updatedPoll);
|
||||
}
|
||||
};
|
||||
|
||||
return <PollItem poll={poll} onOptionClick={onVote} />;
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "Bijna identiek als voorheen, maar nu sturen we `option.id` (de uuid) mee in plaats van een index. En de callback ontvangt een `Option` object in plaats van een string."
|
||||
|
||||
Open `app/poll/[id]/page.tsx` en update:
|
||||
|
||||
```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 = await getPollById(id);
|
||||
|
||||
if (!poll) notFound();
|
||||
|
||||
return (
|
||||
<div className="w-full p-4">
|
||||
<h2 className="text-2xl font-bold mb-4">{poll.question}</h2>
|
||||
<VoteForm poll={poll} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "Het enige verschil met Deel 1: `await` voor `getPollById()` — want die is nu async vanwege Supabase. De rest is exact hetzelfde. Dát is de kracht van dit patroon: de pagina structuur verandert niet als je van data source switcht."
|
||||
|
||||
---
|
||||
|
||||
## Stap 3.9 — API routes aanpassen voor Supabase
|
||||
|
||||
> **Vertel:** "Als laatste moeten we de API routes updaten om met Supabase te werken."
|
||||
|
||||
Open `app/api/polls/[id]/route.ts` en vervang de inhoud:
|
||||
|
||||
```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 = await 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 { optionId } = body;
|
||||
|
||||
if (!optionId) {
|
||||
return NextResponse.json({ error: "optionId is verplicht" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updatedOption = await votePoll(id, optionId);
|
||||
|
||||
if (!updatedOption) {
|
||||
return NextResponse.json(
|
||||
{ error: "Kon stem niet verwerken" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Haal de volledige poll op om terug te sturen
|
||||
const poll = await getPollById(id);
|
||||
return NextResponse.json(poll);
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "De GET route is bijna hetzelfde, maar nu met `await` omdat `getPollById` async is. De POST route leest nu `optionId` uit de body, roept `votePoll` aan, en stuurt de hele updated poll terug."
|
||||
|
||||
---
|
||||
|
||||
## ✅ Check: Alles testen
|
||||
|
||||
> **Check:** Herstart de dev server (stop en `npm run dev`).
|
||||
>
|
||||
> 1. Ga naar `http://localhost:3000` — je ziet de polls uit Supabase
|
||||
> 2. Klik op een poll — je ziet de detail pagina met opties
|
||||
> 3. Stem op een optie — de votes updaten
|
||||
> 4. **Refresh de pagina** — de stem is bewaard!
|
||||
> 5. **Open Supabase Table Editor** — je ziet de updated votes in de `options` tabel
|
||||
>
|
||||
> **Vertel:** "Dat is het grote verschil met onze in-memory data: als je nu de server herstart, of de pagina refresht, zijn je stemmen er nog steeds. Ze staan in een echte database. En als iemand anders de URL opent, ziet die persoon dezelfde data. Dat is de kracht van een database."
|
||||
|
||||
---
|
||||
|
||||
## Stap 3.10 — Bonus: Realtime (als er tijd over is)
|
||||
|
||||
> **Vertel:** "Als je wil dat stemmen LIVE updaten bij andere gebruikers, kan Supabase dat ook. Dit is een bonus — we laten het even zien."
|
||||
|
||||
Open `components/VoteForm.tsx` en voeg een `useEffect` toe voor realtime:
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { Poll, Option } from "@/types";
|
||||
import { PollItem } from "./PollItem";
|
||||
import { useState, useEffect } from "react";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
export function VoteForm({ poll: initialPoll }: { poll: Poll }) {
|
||||
const [poll, setPoll] = useState(initialPoll);
|
||||
|
||||
// Realtime subscription — luister naar vote updates
|
||||
useEffect(() => {
|
||||
const channel = supabase
|
||||
.channel('votes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'UPDATE', schema: 'public', table: 'options' },
|
||||
async () => {
|
||||
const res = await fetch(`/api/polls/${poll.id}`);
|
||||
if (res.ok) {
|
||||
setPoll(await res.json());
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel);
|
||||
};
|
||||
}, [poll.id]);
|
||||
|
||||
const onVote = async (option: Option) => {
|
||||
const response = await fetch(`/api/polls/${poll.id}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ optionId: option.id }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setPoll(await response.json());
|
||||
}
|
||||
};
|
||||
|
||||
return <PollItem poll={poll} onOptionClick={onVote} />;
|
||||
}
|
||||
```
|
||||
|
||||
> **Vertel:** "Nu luistert de VoteForm naar veranderingen in de options tabel. Als iemand anders stemt, update jouw scherm automatisch. Dit is realtime — geen polling, geen refresh nodig. En het zit netjes in het Client Component waar het hoort."
|
||||
|
||||
---
|
||||
|
||||
# AFSLUITING (11:30 – 12:00)
|
||||
|
||||
## Samenvatting
|
||||
|
||||
> **Vertel:** "Wat hebben we vandaag gedaan?"
|
||||
>
|
||||
> 1. **Poll afgemaakt** — stemmen werkt nu echt
|
||||
> 2. **Supabase leren kennen** — tabellen, foreign keys, RLS policies, SQL
|
||||
> 3. **Supabase gekoppeld** — van in-memory array naar echte database
|
||||
> 4. **Data is nu persistent** — overleeft server restarts
|
||||
>
|
||||
> **Vertel:** "Volgende week gaan we authenticatie toevoegen met Supabase Auth, zodat je kunt zien wie er gestemd heeft."
|
||||
|
||||
---
|
||||
|
||||
## Huiswerk
|
||||
|
||||
> **Vertel:** "Voor volgende week:"
|
||||
>
|
||||
> 1. **Maak een `/create` pagina** waar je een nieuwe poll kunt aanmaken
|
||||
> - Formulier met een vraag en minimaal 2 opties
|
||||
> - Sla op in Supabase (INSERT in polls + options tabellen)
|
||||
> - Redirect naar de homepage na het aanmaken
|
||||
> 2. **Voeg een "Nieuwe Poll" link toe** in de navbar
|
||||
> 3. **Extra:** probeer de SQL Editor in Supabase — schrijf queries om:
|
||||
> - De poll met de meeste stemmen te vinden
|
||||
> - Alle opties op te halen gesorteerd op votes (hoog naar laag)
|
||||
@@ -1,149 +0,0 @@
|
||||
# Les 7 — Slide Overzicht
|
||||
## Van In-Memory naar Supabase
|
||||
|
||||
---
|
||||
|
||||
## Slide 1: Titelslide
|
||||
**Layout:** Split (cream links, blauw rechts) — Keynote stijl
|
||||
- NOVI Hogeschool logo
|
||||
- "AI leerlijn"
|
||||
- **Next.js**
|
||||
- **Les 7**
|
||||
|
||||
---
|
||||
|
||||
## Slide 2: Terugblik vorige les
|
||||
**Layout:** Cream + blauw blob rechts
|
||||
- **Titel:** Terugblik vorige les
|
||||
- Links: Wat we gebouwd hebben
|
||||
- Next.js QuickPoll app van scratch
|
||||
- TypeScript types, API routes, pages
|
||||
- Server Components + Client Components
|
||||
- Loading, Error, Not-Found states
|
||||
- Middleware
|
||||
- Rechts: Wat nog mist
|
||||
- Stemmen werkt niet echt
|
||||
- Data verdwijnt bij restart
|
||||
- Geen echte database
|
||||
|
||||
---
|
||||
|
||||
## Slide 3: Planning
|
||||
**Layout:** Gele achtergrond + decoratieve blobs
|
||||
- **Titel:** Planning
|
||||
- Deel 1: Poll afmaken — 30 min
|
||||
- votePoll functie, POST route, bug fix, visuele feedback
|
||||
- Deel 2: Supabase Introductie (No Code) — 45 min
|
||||
- Account, tabellen, foreign keys, RLS, SQL
|
||||
- Pauze — 15 min
|
||||
- Deel 3: Supabase koppelen — 60 min
|
||||
- Client, environment variables, data.ts, testen
|
||||
- Afsluiting — 30 min
|
||||
|
||||
---
|
||||
|
||||
## Slide 4: Wat is Supabase?
|
||||
**Layout:** Cream + blauw blob rechts
|
||||
- **Titel:** Wat is Supabase?
|
||||
- Open-source Firebase alternatief
|
||||
- PostgreSQL database — 30+ jaar, professioneel
|
||||
- Auth — login, registratie, OAuth
|
||||
- Storage — bestanden uploaden
|
||||
- Realtime — live updates
|
||||
- Edge Functions — serverless
|
||||
- Gratis tier voor leren en kleine projecten
|
||||
|
||||
---
|
||||
|
||||
## Slide 5: Van Array naar Database
|
||||
**Layout:** Cream + blauw blob rechts
|
||||
- **Titel:** Van Array naar Database
|
||||
- Links: code block met de oude in-memory array
|
||||
```
|
||||
let polls: Poll[] = [
|
||||
{ id: "1", question: "...", options: [...], votes: [...] }
|
||||
];
|
||||
```
|
||||
- Rechts: twee tabellen
|
||||
- polls: id, question, created_at
|
||||
- options: id, poll_id, text, votes
|
||||
- Pijl van links naar rechts: "Normalisatie"
|
||||
- Uitleg: "Eén array → twee tabellen met een relatie"
|
||||
|
||||
---
|
||||
|
||||
## Slide 6: Live Coding — Deel 1
|
||||
**Layout:** Blauw volledig + cream blob links
|
||||
- **Titel:** Live Coding
|
||||
- **Subtitel:** Deel 1: Poll afmaken
|
||||
- Stappen:
|
||||
- votePoll() functie
|
||||
- POST route werkend
|
||||
- Bug fix: optionIndex
|
||||
- Visuele feedback
|
||||
|
||||
---
|
||||
|
||||
## Slide 7: Supabase Setup — Deel 2
|
||||
**Layout:** Blauw volledig + cream blob links
|
||||
- **Titel:** Supabase Setup
|
||||
- **Subtitel:** Deel 2: No Code — alles via de browser
|
||||
- Stappen:
|
||||
- Project aanmaken
|
||||
- Tabellen: polls + options
|
||||
- Foreign keys & relaties
|
||||
- RLS policies
|
||||
- Testdata + SQL Editor
|
||||
|
||||
---
|
||||
|
||||
## Slide 8: Pauze
|
||||
**Layout:** Cream + grote blauwe cirkel
|
||||
- **Titel:** Pauze
|
||||
- **Subtitel:** 15 minuten
|
||||
- "Deel 1 & 2 staan er. Na de pauze: koppelen aan Next.js!"
|
||||
|
||||
---
|
||||
|
||||
## Slide 9: Live Coding — Deel 3
|
||||
**Layout:** Blauw volledig + cream blob links
|
||||
- **Titel:** Live Coding
|
||||
- **Subtitel:** Deel 3: Supabase × Next.js
|
||||
- Stappen:
|
||||
- @supabase/supabase-js installeren
|
||||
- .env.local + supabase client
|
||||
- data.ts herschrijven
|
||||
- Components + pages aanpassen
|
||||
- Testen: stem → refresh → data bewaard!
|
||||
|
||||
---
|
||||
|
||||
## Slide 10: Vragen?
|
||||
**Layout:** Cream + blauw blob rechts
|
||||
- **Titel:** Vragen?
|
||||
- Reflectievragen:
|
||||
- Snap je het verschil tussen in-memory en database?
|
||||
- Kun je uitleggen wat een foreign key doet?
|
||||
- Begrijp je hoe RLS werkt?
|
||||
|
||||
---
|
||||
|
||||
## Slide 11: Huiswerk
|
||||
**Layout:** Cream + blauw blob rechts
|
||||
- **Titel:** Huiswerk
|
||||
- Opdracht: /create pagina bouwen
|
||||
1. Formulier met vraag + opties
|
||||
2. Opslaan in Supabase (INSERT)
|
||||
3. Navbar link toevoegen
|
||||
4. Redirect na aanmaken
|
||||
- Extra:
|
||||
- SQL queries schrijven (meeste stemmen, sorteren)
|
||||
- Validatie toevoegen
|
||||
|
||||
---
|
||||
|
||||
## Slide 12: Afsluiting
|
||||
**Layout:** Blauw volledig + cream/roze/zwart blobs links
|
||||
- **Titel:** Tot volgende week!
|
||||
- Volgende les: Supabase Auth — inloggen & registreren
|
||||
- "Van array naar echte database. Goed gedaan!"
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,314 +0,0 @@
|
||||
# Les 9 Huiswerk: Bouw je eigen app met de 5 Cursor-technieken
|
||||
|
||||
**Vak:** AI-Assisted Development
|
||||
**Opleiding:** NOVI Hogeschool Utrecht
|
||||
**Deadline:** Voor de volgende les
|
||||
|
||||
---
|
||||
|
||||
## Overzicht
|
||||
|
||||
In de les heb je 5 Cursor-technieken geleerd en toegepast op LinkVault. Nu is het tijd om te laten zien dat je deze technieken zelfstandig kunt toepassen. Je gaat een **eigen app** bouwen van scratch -- geen LinkVault!
|
||||
|
||||
---
|
||||
|
||||
## Kies je app
|
||||
|
||||
Kies een van de volgende projecten (of bedenk er zelf een):
|
||||
|
||||
| Optie | Omschrijving |
|
||||
|-------|-------------|
|
||||
| **Recepten app** | Recepten opslaan met ingredienten, bereidingswijze en tags |
|
||||
| **Film tracker** | Films bijhouden met rating, genre en watchlist |
|
||||
| **Notitie app** | Notities met categorieen en zoekfunctie |
|
||||
| **Eigen idee** | Overleg dit eerst met Tim voordat je begint! |
|
||||
|
||||
**Tech stack:** Next.js 16, TypeScript, Tailwind CSS (net als in de les).
|
||||
|
||||
---
|
||||
|
||||
## Checklist: Wat moet je opleveren?
|
||||
|
||||
Gebruik deze checklist om te controleren of je alles hebt gedaan. Elk punt is **verplicht** (tenzij anders aangegeven).
|
||||
|
||||
- [ ] **1. Cursor Rules** -- `.cursor/rules/` map aangemaakt met minimaal 2 rule bestanden (met YAML frontmatter!)
|
||||
- [ ] **2. Plan Mode** -- Plan Mode gebruikt om je app te plannen (maak een screenshot van je plan)
|
||||
- [ ] **3. Agent Mode** -- Agent Mode gebruikt om minimaal 2 features te bouwen
|
||||
- [ ] **4. Debug Mode** -- Debug Mode gebruikt voor minstens 1 bug (maak een screenshot van je debug sessie)
|
||||
- [ ] **5. Tests** -- Tests geschreven met de agent (minimaal 3 unit tests)
|
||||
- [ ] **6. Code Review** -- "Find Issues" gedraaid en eventuele issues gefixt
|
||||
- [ ] **7. Semantic Commits** -- Semantic commits gemaakt (feat:, fix:, docs:, etc.)
|
||||
- [ ] **8. GitHub** -- Alles gepusht naar een GitHub repository
|
||||
- [ ] **9. (Bonus)** -- Pull Request aangemaakt en door Cursor laten reviewen
|
||||
|
||||
---
|
||||
|
||||
## Wat lever je in?
|
||||
|
||||
1. **GitHub repository link** -- Zorg dat de repo public is (of nodig Tim uit als collaborator)
|
||||
2. **Korte reflectie (200 woorden)** -- Beantwoord deze vragen:
|
||||
- Welke Cursor-technieken werkten het beste voor jou?
|
||||
- Waar liep je vast?
|
||||
- Wat doe je de volgende keer anders?
|
||||
3. **Screenshots** van:
|
||||
- Je Plan Mode plan
|
||||
- Een Debug Mode sessie
|
||||
- Je test resultaten (terminal output)
|
||||
|
||||
---
|
||||
|
||||
## Tips
|
||||
|
||||
- Begin met Plan Mode! Laat Cursor eerst een plan maken voordat je gaat bouwen.
|
||||
- Commit vroeg en vaak. Gebruik semantic commits vanaf het begin.
|
||||
- Loop je vast? Gebruik Debug Mode -- beschrijf de fout en laat Cursor helpen.
|
||||
- Houd je prompt requests in de gaten. Je hebt 500 fast requests per maand op het Student Plan.
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Keyboard Shortcuts Reference
|
||||
|
||||
Alle belangrijke Cursor sneltoetsen op een rij:
|
||||
|
||||
| Actie | Windows / Linux | macOS |
|
||||
|-------|----------------|-------|
|
||||
| Chat openen | `Ctrl+L` | `Cmd+L` |
|
||||
| Plan Mode toggle | `Shift+Tab` | `Shift+Tab` |
|
||||
| Inline Edit | `Ctrl+K` | `Cmd+K` |
|
||||
| Composer | `Ctrl+I` | `Cmd+I` |
|
||||
| Autocomplete accepteren | `Tab` | `Tab` |
|
||||
| Terminal openen | `` Ctrl+` `` | `` Ctrl+` `` |
|
||||
| Command Palette | `Ctrl+Shift+P` | `Cmd+Shift+P` |
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: .cursor/rules Templates
|
||||
|
||||
Maak een `.cursor/rules/` map in je project root en voeg daar `.mdc` bestanden toe. Hieronder staan templates die je kunt kopieren en aanpassen voor je eigen project.
|
||||
|
||||
### project.mdc (algemene projectregels)
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Algemene projectregels en conventies
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Project Regels
|
||||
|
||||
## Tech Stack
|
||||
- Next.js 16 met App Router
|
||||
- TypeScript (strict mode)
|
||||
- Tailwind CSS voor styling
|
||||
|
||||
## Conventies
|
||||
- Gebruik functionele componenten met TypeScript interfaces
|
||||
- Bestandsnamen in kebab-case
|
||||
- Componenten in PascalCase
|
||||
- Gebruik `async/await` in plaats van `.then()` chains
|
||||
|
||||
## Mappenstructuur
|
||||
- `src/app/` -- App Router pagina's en layouts
|
||||
- `src/components/` -- Herbruikbare componenten
|
||||
- `src/lib/` -- Utility functies en types
|
||||
- `src/types/` -- TypeScript type definities
|
||||
```
|
||||
|
||||
### components.mdc (componentregels)
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Regels voor React componenten
|
||||
globs:
|
||||
- "src/components/**/*.tsx"
|
||||
- "src/app/**/*.tsx"
|
||||
---
|
||||
|
||||
# Component Regels
|
||||
|
||||
## Structuur
|
||||
- Elke component heeft een eigen map met index.tsx
|
||||
- Props worden gedefinieerd als TypeScript interface boven de component
|
||||
- Gebruik `"use client"` alleen wanneer nodig (interactieve componenten)
|
||||
|
||||
## Styling
|
||||
- Gebruik Tailwind utility classes
|
||||
- Geen inline styles
|
||||
- Gebruik `cn()` helper voor conditionele classes
|
||||
|
||||
## Toegankelijkheid
|
||||
- Alle images hebben een alt attribuut
|
||||
- Interactieve elementen hebben aria-labels
|
||||
- Gebruik semantische HTML elementen
|
||||
```
|
||||
|
||||
### styles.mdc (styling regels)
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Regels voor styling en Tailwind gebruik
|
||||
globs:
|
||||
- "src/**/*.css"
|
||||
- "src/**/*.tsx"
|
||||
---
|
||||
|
||||
# Styling Regels
|
||||
|
||||
## Tailwind CSS
|
||||
- Gebruik de standaard Tailwind kleurenpalet
|
||||
- Definieer custom kleuren in `tailwind.config.ts`
|
||||
- Gebruik `dark:` variant voor dark mode support
|
||||
- Responsive design: mobile-first (`sm:`, `md:`, `lg:`)
|
||||
|
||||
## CSS Bestanden
|
||||
- Gebruik `globals.css` alleen voor CSS variabelen en base styles
|
||||
- Geen component-specifieke CSS bestanden -- gebruik Tailwind classes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix C: Prompt Templates
|
||||
|
||||
Goede prompts maken het verschil. Hieronder staan templates die je kunt gebruiken voor elke techniek.
|
||||
|
||||
### 1. Plan Mode prompt
|
||||
|
||||
```
|
||||
Ik wil een [app naam] bouwen met Next.js 16, TypeScript en Tailwind CSS.
|
||||
|
||||
De app moet het volgende kunnen:
|
||||
- [Feature 1]
|
||||
- [Feature 2]
|
||||
- [Feature 3]
|
||||
|
||||
Maak een gedetailleerd plan met:
|
||||
1. Mappenstructuur
|
||||
2. Benodigde componenten
|
||||
3. Data model / types
|
||||
4. Stappen om te bouwen (in volgorde)
|
||||
|
||||
Gebruik de App Router en server components waar mogelijk.
|
||||
```
|
||||
|
||||
### 2. Agent Mode feature prompt
|
||||
|
||||
```
|
||||
Bouw de [feature naam] feature voor mijn app.
|
||||
|
||||
Wat het moet doen:
|
||||
- [Beschrijving van de feature]
|
||||
- [Verwacht gedrag]
|
||||
- [Edge cases]
|
||||
|
||||
Gebruik bestaande types uit src/types/ en componenten uit src/components/.
|
||||
Volg de regels in .cursor/rules/.
|
||||
```
|
||||
|
||||
### 3. Debug Mode prompt
|
||||
|
||||
```
|
||||
Ik krijg de volgende fout:
|
||||
|
||||
[Plak hier de foutmelding of beschrijf het onverwachte gedrag]
|
||||
|
||||
Wat ik verwacht: [beschrijf het verwachte gedrag]
|
||||
Wat er gebeurt: [beschrijf wat er daadwerkelijk gebeurt]
|
||||
|
||||
Het relevante bestand is [bestandsnaam]. Kun je de bug vinden en fixen?
|
||||
```
|
||||
|
||||
### 4. Test writing prompt
|
||||
|
||||
```
|
||||
Schrijf unit tests voor [component/functie naam].
|
||||
|
||||
Test minimaal:
|
||||
- Het happy path (normaal gebruik)
|
||||
- Edge cases (lege input, lange strings, etc.)
|
||||
- Error handling
|
||||
|
||||
Gebruik de bestaande test setup in het project.
|
||||
```
|
||||
|
||||
### 5. Semantic commits prompt
|
||||
|
||||
```
|
||||
Maak een semantic commit message voor de huidige wijzigingen.
|
||||
|
||||
Gebruik dit formaat:
|
||||
- feat: voor nieuwe features
|
||||
- fix: voor bugfixes
|
||||
- docs: voor documentatie
|
||||
- style: voor opmaak (geen code wijzigingen)
|
||||
- refactor: voor code refactoring
|
||||
- test: voor tests
|
||||
- chore: voor overige taken
|
||||
|
||||
Houd de message kort en beschrijvend in het Engels.
|
||||
```
|
||||
|
||||
### 6. Code review prompt
|
||||
|
||||
```
|
||||
Review de code in [bestandsnaam of map].
|
||||
|
||||
Let op:
|
||||
- TypeScript best practices
|
||||
- Performance issues
|
||||
- Security problemen
|
||||
- Toegankelijkheid
|
||||
- Code duplicatie
|
||||
|
||||
Geef concrete suggesties voor verbeteringen.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix D: Troubleshooting
|
||||
|
||||
### Plan Mode verschijnt niet
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|----------|
|
||||
| Plan Mode optie niet zichtbaar | Zorg dat je Cursor versie up-to-date is (Help > Check for Updates) |
|
||||
| Toggle doet niets | Probeer `Shift+Tab` in het chat venster, niet in de editor |
|
||||
| Plan wordt niet gegenereerd | Zorg dat je prompt duidelijk genoeg is -- geef context over wat je wilt bouwen |
|
||||
|
||||
### Agent Mode voert niets uit
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|----------|
|
||||
| Agent maakt geen bestanden aan | Controleer of Agent Mode actief is (niet Ask Mode) in de chat dropdown |
|
||||
| Agent stopt halverwege | Druk op "Continue" of geef een follow-up prompt |
|
||||
| Agent maakt fouten | Wees specifieker in je prompt, verwijs naar bestaande bestanden |
|
||||
|
||||
### .cursor/rules worden niet opgepikt
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|----------|
|
||||
| Rules hebben geen effect | Controleer dat de map `.cursor/rules/` heet (niet `.cursor/rule/`) |
|
||||
| YAML frontmatter fout | Controleer de `---` blokken bovenaan elk `.mdc` bestand |
|
||||
| Globs matchen niet | Check je glob patronen -- `src/**/*.tsx` matcht alles onder src/ |
|
||||
| Bestanden staan verkeerd | De `.cursor/` map moet in je project root staan |
|
||||
|
||||
### Tests draaien niet
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|----------|
|
||||
| `npm test` geeft fout | Controleer of Vitest of Jest is geinstalleerd: `npm install -D vitest` |
|
||||
| Tests worden niet gevonden | Bestandsnaam moet eindigen op `.test.ts` of `.test.tsx` |
|
||||
| Import errors in tests | Controleer je `tsconfig.json` paths en test configuratie |
|
||||
| Module not found | Voer `npm install` uit om dependencies te installeren |
|
||||
|
||||
### Git / GitHub problemen
|
||||
|
||||
| Probleem | Oplossing |
|
||||
|----------|----------|
|
||||
| `git push` geeft permission denied | Controleer of je SSH key of personal access token is ingesteld |
|
||||
| Repo niet gevonden op GitHub | Maak eerst een repo aan op github.com en volg de instructies |
|
||||
| Merge conflicts | Gebruik `git status` om conflicten te zien, los ze op in Cursor |
|
||||
| Vergeten te committen | `git add .` en daarna `git commit -m "feat: beschrijving"` |
|
||||
|
||||
---
|
||||
|
||||
Veel succes en plezier met bouwen! Kom je er niet uit, stel je vraag in het les-kanaal.
|
||||
@@ -1,219 +0,0 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 5 0 R /F4 7 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
6 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
|
||||
7 0 obj
|
||||
<<
|
||||
/BaseFont /Courier-Bold /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 16 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 16 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Author (\(anonymous\)) /CreationDate (D:20260422070906+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260422070906+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Count 8 /Kids [ 4 0 R 6 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2149
|
||||
>>
|
||||
stream
|
||||
Gb!#\968iG&AII3C_)qR1'rO[$0K[XZUpU*RTO\b^<dQF;kcg:V!Q[I^'fHEac``MFD*EF"9JC+m!\p:MZ9+A9GRVH`piO$"3+bs"\]O8j"KnN4Sh&^Jju2$+@65m&4!U!4H`N97l_2t((ni:!%Lm"LFN;L8J$V-K[RZdZOr$4qN*2/3KBW/+R82;M#nRq@tFug1e>IuMsLpLYE<'XJ:'J-jQXGZ0d6sBO\&*#%K[tC0g`DV3%R=7["1]!h_I/12-+I-A42$h7Nt\LeNfou*Dquop]Ii@+ouIAeO^>RdE62\?GO\IfCMf-Z'^t0\LraJriSnX>h<r2GmLT<]m*g_BiUW/]JDJn/!^R,f2#g;KCL`JHGEm.AR2[Zs,3j!Jrct>\i&U)Zi)DGVKiSd^ulX@W`(`f'FjRc=feg77KeV'[@a3AQNjud*FEC_4eC/bAEhNrL8#cd&k#;7E0Bl-e8dOt6ujnGap:A669oH^WVdrh:XJ&Q!4Y6%"8eX47+<tbS?r^-iQ!aq7o)fB\YmgWH"^ct$GF63:EJd<q`qajVCoW/QeecR3+en/#]>7=fHY_^NL(_HE0i](SWAcoXMr)'rED)7pHG>_G[Ggd[pF15Dr"'59,iq8F0qX58F(?N%bc=h"O03&S7KCl1$,3?5^7hZ66LhBL7R5`d-pN[IeR!&KdO/S)[5H@9#Q@BEeM"?$L'po/>6XT>q"PDX':r/0^7*J&20BS0JuN4.!W?RX.ju=j2&r<1H5-IJSL7-dNK3ml[.fngCWpn_p8cTYrn_W9Nh=1l7ct&\H9Xb5_eX^=AIi94J'<.l?TRYJs-9daY!ZLBo]1Mf4ARB8X`GaBt9uU*hXeiNiM2&'2Pnj2qsQZ08e3?`D)J'VVmfb<!R)-`DJK4ULTssj()r8:X)*]R8q%Sk:lmZC!Mks?.Sd6::&[h&*_iTZ\N[hTdBikg6K9*PXPCk>]n;U_<7?u)]N\H'dW,2db.Ck@_dIhq;,FkR4GZ"(YZ&OVS#P4asE`a06N8^fSC5'-rQ[Xn4<]5k0d:t/Hl<T-8VNh!.M$X1H!@cWhEP3QSb>,a/c9gE4UYNnu/6WB+l[/:n-%)P:Wl!oIe+D8R,IK'6@eo+Q:UV=-tZ:,b;f"Q.K%H8uYKZi3RuKN+<.e2;-JliCR)_[j9ZR<mA?"T%q7E<(RtZR%+Y$9J+H5Lu/^3^1>9Aq4B,4*'M\a^0`MA"1^M2\b5"n?/fqCbI4i`NsprcZ7KV?)KFluUpu"L1GS\F9^1PH.5%4+g9#5Hs4K(rG@CR*S@*$d^_d@m2TGh&^gFJ"K=4"_!0!s%<+A$Y[*EpBM".D[@4+V4`iN`f-a]eINb@>iYsfAo/jE11o`L"Z5E^(+Fc_3apOEa?\NQ/epSdM_&^X?!k=@@eH$<>'W'[nOBT99C$t$`@YJWVQ2[3aBT"X>dF9fbU&ABPFldY8p2TD6`eDkd,Y_SS0lip\mL5:Je*OE5e?TXD)Q2,;1Z`FC#Cp1O5Pf\*i/='9O@hl4e&tMmg8U;PEaDpT-T)[6VU$=P,@DTDF5G%;_'.<p#a@C3kLGZmQ;BgQDjhi]rUeGr=Ad+Ocm(-;-flJ4N?@gZla$u2XcMUo$4>K=)qg9NHOqsraS`0r8/1VbjP&1q:3SP3`g=)ciNQVQBr*<an=0"I:HS00M4+d.o?!C)Pk("(`PjhlcQC00q;N!A("?=s=j)fk.Yp^RDid09YT^Wj!Ad?!2pu4J^H*C5*17JV7=:&!:T[lY5:VkX2GY?Gt2HdV/iqqTc0C-j/P>o.8WHi%3T<Q(#dPautPeq2<Ds(!H?Bb&bCO7laJ^dQGs&_?s(eB>sc&eu482<4FM"X1`fY1qe<bgBR2kWP`O7q!a<A2KNh(He;AZ^W:W2.68%NXKPN_ulBc$!(C9fG5tASb!4Oe_V)XqB/REaug*oe];5gfckL]6+A"A0L(A@lkd_R*b6s*`s48YV'1AKaoLM_E(En/I%7d_9ftEA(k!,["/ClB-UC%?WaNJfHG$U$B$!1+poqAD,n_`*8QomTMF5Hi7rd#,_k5JW*arqZ>QN=T3F7krZ%tFS5fh!RZ$.D;f_YkP0LrojS*"ngC;l?b@=eNhHl[^DO^/Hpjr*`;&EB1Me-M)R%+$J%D?3B+5E38C&~>endstream
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2409
|
||||
>>
|
||||
stream
|
||||
Gb"/)lZ:ji&c]#K@[!5(S37T\J07DF9?25RC6d;RYP#8t00Bkk7idV#pc,\Wla$s_V:/Qpf,B!F8L'@Blsb1D5O8Fo#7#ahr?#qYT$?699+G4BHjO%5$1frt0gGL$KXjhp3"hVEWBe#0!nS*ZKQ;3u+`Y:b-Fcn1KcUoAVa(NbI%U:4C03Y`@:;[p4'iI(Hj];@E28gO</q%*?02>t!0RYLLX`)q&!5QU!Vs+p@8$XgeHtj*&t/M)$^BW`4O+r*.KNcC"R=ouP<&g7:HC'0;DMd_dud3Ac3]"mM)+']Sq@cJ,X_^j=_\em<@mW1UA5R:^\m,!Dr>cPdT*s*Es)HD9-(3O3#BGeVf,>&<`N>R3e<pq:7JoKlE#F;o*^pMVP,C8V=;PofD)!&$:9!nUt):o2_Ednd43Bb&/.7Bi^=6-W_QAt.#+&[EItH@1)0_ABKP;$4gKX0H&$.#^ppSDchF$m$>-L6U286e,l2]onQNsPK??KoKc8-[<gUAEV9U2j_M*`c(mKKO(H&:9&k*[j(M3b<PLYaq@)[Wd;@Gq=G.K4uMWX5n7]C!=5-&i"H[='i4c?U6QPCXtQ!U/(Zh$]RMFF]#`'rEAIL%D7':Uti\Xho,3:$QQE;k^5']Yc<K9l"^KuQ-'3a*A[IN$fLS$YFfPh;8Fao\)/JHZ-<@#jFHU,@:%c^>jX_-!!7:E"Rp1TB].N%]P6LRK(U%0a'sQub\AD]j(^bYV<k^O8(BV\n"SiA"jj5NjZOe,,Fg@MH5"3??s[O<6Ut$=$a7E4rOJJ3DjP:`XMs[E5blOUEP$,8i73@)dOh3PA<Tps$"or5[oX#FpO`*D>B?/3Rpo.PKadK,D/@h$N:N^"_g,A$MkU;A.6<)i1q"dGeRR7@P4()ZS5:`KC8/1m5[A=#D-K6U5)WJ>Rf\rV6kf-";*<e[7$E4+FUBVX$\h5pb%`W#e2M^,W(JROEf\o0aZeeBe4@P)X$2H;Uan$4:5G[V%0O&43&[[>IV)<9q2O*>2`C!Ai7$:,!7iAOb\)0MrAUpbUjGrBu%+%sQUYK8KL?PWW5[')GImN'4?%A.hS%3i\Y^dlR]W,&])VG!$8I5^jU]*l>mg#nHNaLPqesU02mY#\/o-#eM)J2M;-Ll`pjfV]J@IMo7BO1i<Wm?/gV%"ZVoJ4iE=u'.D^=&i\s_G<!*6Z&N/;5t]jjgYE+O-F0<Jn^5Ae(RcnW0lCD!4/'G)/4=8;T]"QmJbhu8n=5*H42";[jWQ1&s"[$$^T"5&=5VK[<uFG@!XK-"7bqaJM:AtCj"gROI0UHG53"DGL]uZa$N.(1"n2+Bb3Cg<kP9:j*Y&QF^UWY9rSc9%N4'9^rWU[KnUH_B'g?^[Zb$>D)/u*4ruie<UMpLQ&idNrldsD@i-*g\KWK:C<9kI.-n=UZTd<%K'mZ(qH2550rP$&#OhSl#leA!"c+oDao0^j;-Xbkm`)WPNKgK@!Qg0iLCHIUCMZep2BuIqdeN7csB^i!MPV3;urpnKQ0--Gdl#(GN&A9]'!"[YUHTXF6Z_eWq0l=ON1&]n#GhX'fRF6#@kM10S2n>nQ0I4$+mOgq%KNi6!+QZ_G*%VPtE!%FMjmSn;n)/q1LQC;.?Vt/&B'8mI=P17UfnZL1KDAbDa2_off>Dfn_rqB[EV0:g^7=RD+^i`7F(cTj:QAsWAG9Y'!_'FL(n8t]_f<DD/5O\/bgS.#\3l0JKi-TlBT\Vno"H8"n#PZ/c`3^InFsSs8_g..<2LQ['oiB_kUq_[O5Fm&opNh4_tIiKX*5lW7"7<c"VFLXjIgHjm<I!Y2jL1=F`u&Y+$OAEd@GcZeiI,MWSX;3e#beoq\j\%F>p,Pj/%[H3Q@a(i:e?D`@$l7JfK+R<V6reLsWgO8^LIY9l_Li(kXW#0,onDCTmc!&M9*ShYZm%U**DpFSHh>lS<]LS,qK2)"42/r(opA&aOM+oP]%r#=]M37gL;jHP#6A3rS,][656f65\d+034e<-sU>``mcpCd.\mK2U*EJGuGMRIE_8;W<&2<fMB]eDhiqQ?/1TR.@Xj>kqc,HY0F8J7m%dWQ(Ha]$`3u,N+GEa;IA@BZt)POB.d9%aG==]1(6GPbgPk,oSfd-N0K@a);Bs<ku?98HT5'kgX7O;IXm,-F@;VWfK>$eZoY&3*RcCA]lO:+rD1!@R&@neDXA%Mop066f'%L&JC8X\(?&`QjY+,-TefQ':C)Si6n:F&lP+\c0j@N4p=?>DA81,\DPo3mja'@0d15am%.i;1=4R!"1bL*%Z3q^%e9,YAGs8Cj".'>\CoCUk6eh6B8ZV1j]q#?c'7JHc'`4W=cC9KS(M1<4#WjrlX"Y>64tAr3,@oI<^c[S/bG^kO.rZ*@ljpc4AL%`Af1e$r3mM'omsia8-@^"*#>c"@X8~>endstream
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2315
|
||||
>>
|
||||
stream
|
||||
GauHMgN)%,&:N/3lqAa),osh3n<u;V]d->#\<5LZpn86&9=-.2-)!<tq_!,[,#PGbAiqsS!P@N&Z+C"e$n2+Zibu%)GeJWcT,96lMd'R)Z!d)CIU#SGSe+29O'4f%m<FWkiX)+O(1c`&N5?Y-$"r#ogU@K5D$N$JNpAG:1'P-?-cE4L?#"^-a-JO9nQU9sp^VFs0gUJ*HXIG'1Z-D)jZO:o5+nKFhCmtTrB)5]4J3=R'$kQb"Z@Ti$\uV8ClW;(Icn<Kk*'PFSlqjbS8+&%69+62Yq4sl82eF?QskdPXKkfX+roUqL='H<dpG?=nJ9t,CR#-o,`1H'SZ-"liJ)Ci0&Or,o/t7)>/:a7Z\jofX()#/(3d!-BqRQa/T)L@*U&!VEq&&40$GD:G$h8_QKp5j+?Fgt_i8De3YbLnE.XBtEJ+/_Z,;0F@rO)MikPmWn\Ps:QYEJZ<VQ_M?bnhs\4gGAK)$b8FKj6i*g>VhW8H^R>fUk&*4CW-/DT39%=0?YT@#UbP"uc@=K4p3lR]]\=)bRjng&iM<fp.Ac;b9E$47^lIs%2f]rRIfFpL9=.^UpD\O7:i;%5f*VW_e28o;DME&EpmfSRC1(NXH:2LK@=IZNB2T%EZf#hfcdV-)4.(dT!aN?XD;e3KB2$5[PY.lJF%9%$%=?l7fA>0j2e>-JKE"+S%<Gk)h5nuJ%HM=Q6q9mF\DfYhV-<mP3Wh48%7naB<+6d<?aE1<?lXefi[9PRojJ!+Di?Ls@/%q'dTE&4?36Ps7=n7^\>g=e&sJ9dl2`NE#V)5;u9;MQZq,1*#El0HQ+R-IrH1u3Y9nLAh*i<%kkna=Rl'Z"'a@E*a7b#S]gk6JS4+]FkiopZC$TRW597%7RF1p/`7A,]<0M?fO?M4!9&;eOMPmg%Kf,e36A#L/W6YVAgsNB%n@,V'$Z=g+SDmeK9/K*BOZiN<*.+dW:)qQ1<g=m7=UqDOAN=M^'!:_0eo'/>PnkdplX*!m*<oJ@IBGK3Qi4kZ](Y-RY2Hm7E%/&"/b`KANoLo8(.l8T+AEi5*$%2gUs5R)d3[E=4LcZ,-$TIBj?TlB!N8TUT2/`-#m_.V+S$*8*j^Wp'SS"MM-()\L6[h>mWf+OWQ%lf'YbE_1?E.-T9UZK\JLqhm:h14+!H'.!-hD3_@O6?AJhArn942gliGXmYZ9@6dT+0't#&(JGKLM9SP=7)TACHCRPac$U.RG*TYSU14k)N!Fu:p5+oH/(=^#DPR2H)2Z=S)[?$\#QoAYlCLT%mW`,bO_dT'rsE]!mB^o\D1sfK#tZZY*oRI'JF]j>/0hBhPh52D:(tT#-(RI*2oCNC4m(]H$GX9>192G`Dg%R6LmS>cse<@+`b-"e5Td)TQ[4DqYa4TMF=5AiGmYS-`RsG,K+3<1XUJqK%3U5Xi0f,'p,(:B3\uZ8EI'PGd'fB#1Qd,aub:M:mkZk;p#G>\#+1,mD/@HPkRTaV8+uLa#>3k4bad'@&n;T>F@ep_juB99C>KI+\NFWW9JAtf!(;mqb$$p'cbW.<4U!ONflG#"Q"e]9<d/UfVUWeD[]oU$!*`T\G*U;_gR]&2o6>T))9c'0pR$e@WL=k!^CfcGA,7$g"`m!>#ZB;PmtqEiFXR7[G'WV?CN9?CDGqi0D[Z]!aXR1Wsg:tVG'FP/5pP?ai,jL^[?'ug$ec*0F&8O5R5+n#D-DW&5G6%3WXQ.Bd4$Q`tQ,LcrQ;F3eIJkl7Xru_J4&B<b7Hr\L'3j]s;T4]E3Vo[^e=uA_b2s`*mDGARWT9rHbmoJet!V4+2ZFE2TjXH@cI48-+))=5\4T'CYfuJ[8J/F>fW]!nlR(Lf`;&'cij4R&\k%rY7=&a#CFT!dpj8+:D*g-?3084\Zr,na`XAR`Gi!pp5j#fTW-?D`h(@i@Uf0q3Y[?N#i5nf%I4hk(gV=7//YZa4NpJcj/$cgA`\I1UsUYpSVK(HVs*lhuC>K`uf):cgYl_2DE`<\F5-Y8+R8d<IN9U/iQ;<<bXf8L/5odpJi>tJ2NYMb[Bt.Wd,3KGS]C%$ZU6sa\)4G5_]:]/[rP_L=&AE#K5/?NqWSGNuS$7L%2a@!;e,"jcAV1=+c[12kFsdXHqNl]SG6I*12tnnG"k`)>k1abgOMi(3@-UTtB.'TV%Q4T$+f"mr3P'nanO0_L&\Tb?5tZhR%*[kAjWE.j4i!bQA_gS\_eTo!ijUAH!DN(\0kH)FBUMcNK'\W;'tNo)aN'%-.<hF=cnIc`go),f[V)Bc]1"Ap/a^mJ<%%B\(HbZuMO@^hhRnY`KY#3rc@mYXJ%:3VL5RhhiMCquab)>'B~>endstream
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1534
|
||||
>>
|
||||
stream
|
||||
GauHLgMZ%0&:O:S$kb+Q2:f-*M3O76FPBLk/D<hXD-uiXagArTQ<[:1R_S<Q](hFVEOTs#5SReSbi-d-k9q#ar1itS/-(-f_"\9@b$Zt,l4FOR?f@?ni3*0$SM5"F+>+I]E.J-EB;#RLI#t%A5XWC:'>Qt1R*Ppi7YL5/*sr=\iIHTh^$kG5?oTpa7C]Vs815GYPe+dBGQ8dnS;-0cb!&(32[&bOfnYjkmH&k;GX<dl6NSF0#FlE^\UqX!35[U5iRY=LmKW>6"1-(1LjG'!_f/?N`:FNb7cFnATEZPXcoP*B'N^BH)aoknSGi<=n(2NDc_XAXniVWpAKd7m'(-Y/iSo0*Ts`=FSANTm/^o*XOBMAk-@`mCBL6Y5pD>h<g@P>EC'`d5V=]s&*\4_WU:JDjn@&8M@%gbsJoI''/gBE\1[/D#n`LFf6o-6d30flbUkC!n92(sCWs*_bQ8tOtP8#AfiL"5IGTNoW@oWVtD<:n_c%K$&Egf;HCMclYNfB7<"8jm,D"'`&dhc*lV7o35qd\G3ke.Eb_F`9"Na6g./).q$D.2!RE0D):\tI1Lp+UW;gWM\=DT*W8Z,<dQj"E"fk-jf,2?I/0j!7t_o*d.La"SFJPq:p(V&nY&OgVsGCo:#e]82j+/#$"^(<ZALV5eotfd]WeKfp]do$9t.]<G,0="H8$75Mcl];/9HdXm"KS>f1">VG$jm(pPNY'-?AIH"(@S,)^nI+l;bCTs?s@=R'>K3,1*2(,"Z'@&Li-&ZI=pR;DASBFmhIsa&5mI'QI[Y<nC]8uf^0l*[(fqfnF32-H2KJ8Sm*LJQu*1MekZ#m(njEQ%),&0'CRJm$=9LZ:5l2uB_I*o/uZ+Z"p.(-HH"d5qoSJU<qQ"%Z!f"Q4?nD6(IB[dp@'igpY]N$SmHm3"N7#P-5!th4u#GQdU.Ir_VLFUWqD8`oW_\q7`AqtN#(O.$idN3S3;9!n`4AYQN*rttEmlVX$a$Wbi^JDEO]\`FV]7=<sV`]&i$2SDW;u#-YZL3:HYX7tO)rHO#WBjs>'sf<N&EdgSY3?^kFogR'2^"I81-(l+Zo$q`7QuAO9dRZ%MR#pu!j[3Rk$<4X<#Z:<8Yjc^Ku`S\-MSjG[-jga*[Sf0AcU=)6FdI716+(.-6lAk6<'8)MH?k($MC>OY"b*J7bk'kaAaeYU"#`m$5JQK."8:@hintLO7=V-hNSlPIF,^t?R:FZT93u*H!r$LS&=4MK)'3jb&Psci&g3j"T.Hc(VNZ^33hbcVd&H'2[a:N*UE<alGDHX<%tX#:_t^qpSfc'ANu"?O01$[,O-@P(/=D\[V_)7H$.sa7,Is9`iu0]dJD<XlCp5cJ#A%7d=/"'k`+\"iN=g.qpOI0N/h@6QVfZ#U11^u\&VIN$CNK@<-AmVO-G)D2+edhf&h<]X*JjuM`b%>*6hHV9>_*po1uTXi[(.fGE>/+*.MZ>^"$=3]p+AB.JYCNoA(G*dUqqgq;3:=2e=dl1eP1/I>n`CLPN^QNXGXoN]>JX%CV&.59'CKQi~>endstream
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1744
|
||||
>>
|
||||
stream
|
||||
Gb!Sl968iG&AII3lm4G_VM5?f!6JJ,G$`HN8Y'$Z%SdKJ\Ya*2U$s5's1Sk;P[l(PSV6^1@um;E&D.MODtdIlK0t.DIRZ?5nI29J3H,#*$fI1>R(\%eZcSO?F.L*j,VW;6?b=+$=,B1/PtVORm.5`6/i9Ll1#XPj687ePgk,smilQiWlhR-IHsb5K\C/3:^1;e<1iVk<f2k.,-57!7)r5n6VRllU3o0_,S,_*@#ube;Mg,a'[H8)9B9$r=J/jfH4ZL:'ZOG,+lWs@!P6'.u4YfUs*$:Q4KTSAGLE:DeO='I>Uc)Oac$X(6c#PdgpZI=`E)J#b8!deT_;@t@d:EEPK^dp`p#/bRVfdVH4NA1KjYFlNGmq#0Qp!1,GLi*IR1Sbfrs!6Ei1s*5CTJ<-ZDV,o(*TWIFl(S5D1Ln/:sTl8,3dJU&"hkl$4!s.4+/Q.gFrrWKS-&]&rSaJ$I><\BiX<=#%j5UZ`q:B#*/-rd=L(A_EBJ6%f>9M&n70l^^r4@o:;hZ/=]cf`X#3FUkV!:?icVH^^;SQLbrU\P%%E,0ih+0@9>eDUP1Zr\3a<)70<kAX#;iA=(PM8AddsLF3r9"(+Clg*VQE0T@fdk7(UQ&bJs_1i%u[oDp-B8LB&`(8Je2Z%Ig,Q&jr;uk8@/571DXf\ti$I1b/f&$knZnaN2Gq-rWSQ4Ap:I.=lnKCi'X/f&4K7CtJ<HbI,)j83"oGKeXcT^l2F2YVD[89ZO:R=mfknftZ&HN^Xu)-%((A`T.WYn=,t'DF4j0j1<646p9Hq@s+WeZ!m!ZV'DBk#I17VI@TPX\`^.lf6=dkg&/B8m[pZ:PLftfc!@sN_<V80V\pV>mQ=`,Ta8R61X+&9R\&S.ick1*@95;uijW,fhB:6e,XetgQ?X0F.4$i;R"]6tSPk]ESXnT^I02_p,6<<\2m+4Dc#X"$n@YP>gM0St$.6lGJc^k.K.)Bi$,n'X^Vm?jM1b`tTM&oKb3c\QK._e].(/*_*VAP[#`&%S'RC^Q$_+^6#uF!WOs*j\/5=*\g`<2P)N-m,&T:#]aE:Fh;:Du(OBZo[('t2c=Hmfo:s)0O9I1uZK>ipn0c$Hf#1(16Uu[LiK./WVZYo4oD"`hK=Y_sPCq-1_WK679or!GLYU-*H@f5ln)_CGrlP?;e9o!@+oDKK(EU.!,eTQNSm>-X)e+F@jl^YV'?Qi+p]0UJPI>&:mq?j(mq?h-.#/(CSR(gFrS%c`Vc??'pf=]/<_BR_+'up*,^1&pA4!n,4SaguTaB`<'>IMAme#U?!i39AF-?[5;5+9-4$p-V]_B,'DU#]]ugp+Ea<U&$`/J;0J#&JAI@8r:AI),#.1j;]XUaf%BrJmb-jS)*5O:6e"eq5SF;nYIPN@&\L\f2g:dG>i,4K[$sRl4)EdF;@K#E_-sQlVIJ+P8fWVo*42(<8V#`Yi5eQs`qs.0,:[W\1>deRnS>'Xp],d#)h8o38%e:pob!pf6Q7/Ha;QjN"FO!T*os^P_4N2\]'YIDqCj8+EDA'Mljl`?W:Rc?/EApSiBAd8oeYq?b+[9j[$1]qFA#agnt*?'O_q)a9k]$s'1OAENeHUH?M-MA<5%QX^epi;B58<CXj)lE8]Ap(YeR59Oa:BoIU/EDCJ5D.QP2?X5BZY82>50q&@W`dBUk3MC>i_@3D^0t/&GGZ5:')U?slCYM-=N6(qE(?S\$@^rDie5+%/H1ZTKI`:=@].8iDR,V)\4,&7-^LfI-$i~>endstream
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1516
|
||||
>>
|
||||
stream
|
||||
Gb"/&hf%7-&:X@\EA%LN?suSuUnk\H>MBkU[^P_7LLq(&9\YO!CD%@8qs/I%r$tO:$F<WH^_"9i"lA'*hXUK'$n2*0pR>[nhbE%g%0OUY1'cqT+"$I8ojm^8Lj1``W=Qo\16`iAckAP4Tj!Cq1_*YV39(W>,R=p6"O;i4!Z"%t@"(!XdDeAC+q-JlS-^)#m<oU=.Y0=8GbGDZ-X<msY%[2i2"r1Z5HT@`g4+E1+p-?Z,bQEIP5-4;U"khO1(YH"5bisM.9qij]+"'R,9f'n,X-sX+V<O$N#5,_r4jjM1glf:g-tNW$&(f)a^G%qcbI2Hb@711A6BTqr*h.@/.Gj**l+h#kHF/Sf1+$C#Bd)qU<HSS?Gbb-->,7/JUa:"HULC'hl@Y>\869#<n43mq]T;049g)hk9D<g#?5Sk#!O>XOoZo^k)fhq%i?:IpNK3F[rF(Xn80be;]>N9-E8u.Y-cUP!m7Oa8okNZ&I.2#hZM*h!JVChL![ZSdo9b%'*Ch+92)$IWsjC23i@FWP8#AV&X[;@ali&8XK9e/qmhAj5@N?VB)?:-4=7tZ,NdC%#WkW)JK<4A@lpB29BpmO3(dFC\#VChFf5sU_Ja(KO'\c4p0_Nd\Pn7CEDucFd2)5^S[C=Nh(c@+ES$Ab<:J_?gn$<6R:53c#nB,\"n%!!)ik[!iO*YSp)paBDB4eAP%CJ.k0W2CJAH'RE*F"kHqlBI7?3X[0lQX$B_=H41@p;-Dr!*tTIWW*#E%O+3n(Xbi]-nF/#8C3Kfk,`ARHCBjW0Sj=i0hk0.In_(2@#1H?K1l$]*_n!I6h4-qN>9H00FC.\,_8Cs4i"`]6e/;rMl&]A\!%<?T/X.]g-kSBk)lgD,s&Pq$92<S?+bY:rm/`Ia"fLWWmXW0M)>LDT796V;30>uGD@_JT>#"X<G[$Vof36/Y@HEGfk"8->8L;Wu=&52ul-SsT<X^W6:>*-8l3/9]g0'?C&2m^Z[a_V\NpkOU^Eh]gcq5L06/pNo7^dZ8C\_FhJ-Buj7Za4T+Q1,)Xt(PNC=8]sTL1fDY'hu'=UA/6Z`PagV7Y`f@t;mdVZi!I^Ml^LZ"fa?PFhHQ<O0sFm>FP6HT#p;"/bq,TAnQ]oU4hV(_"IJI[X(I@aLaGiml9;4m3^3LT?[1%3JZHD&D+4#+>dGWRqZ=[CNU?7Gm#q39k/lO223/DL<.+co`QXKECXc9l:BkNUXtN&lGkR9\P>U:sX\sRU't:3KXCU=Ub57+):BSI#`h!l[04_ZuVi%l-n9pe_>HQJiFdh>(ILkCeSc1S6oVXUTQt1E'0i=QK4.4F[4H;3Gk)dPnp\SbdWW#6M4HqJ5a2_(TG)G?gl?j)*]2Do\PqJ'^eF=G#=6cScC^,6GT#1e5\[EVV0Y_bdoMaWJ!ahq&WO04B`.:u8H\CRRk7'I$e0<1>.b-JYV:)]Ro[\+h3GZc:-XW&&s4h*_c&j.>Uf%ESnBD$(Ghh8TO[g^eiqM_pf(qriisZ#F(:IBNUV?iGjU&*~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2279
|
||||
>>
|
||||
stream
|
||||
Gb"/(D/\/e&H;*)E>VLpc"_s.W4.3mOLArRTj>:MD6Y6kA=8_u9;3cT8[)R:,P>O![::SHM[g,H7%m0.4ZqTEj"gjj5?.Xl&qCcC3-e5L1)N`:EeD9n*W.r:r&l`-ijj_p@9U*TRp`b'`B4WU3(S%q90Io)AqT2J#'2=7(c-!KTap,bVT/eVX7n]oEa=Dq7P3A!mcT=;',r>W%F_^ii!-NdVcHdo?/0=DJ9%[g?FBg^Rt5"a2%">ZiS$3O?r;P1dq4\l`UF;Slb&/7Hr-H`<JU2T]0<"+;&sYmi#FJG-B$diWY".o>$J#W@bQQP.7Bp6Ndh#tjXm"E:_^`>ar!=/\Hldn>VP<Ul.b02;+*i%-LaW4M-+AH8B[nb\qJJ9;VdJ]RAETa-NBVb^k8s^<*Y?/?A\XELJd>hduj5,?;q<^Y(8V\1XM9p?L[@VfKhg=<dIPJ1SEGq9RFY(BpOTt'&C@(k:k'?@N::-%pVS<\,p.r&7Tj7:rh].L%XUi&DdJfS5N_GdDsm8iOt`e\.Ue?4h=<[80^uLLXE\gCNX#I)%/8GPWg,l>qIHJLnbC7M;t`A3;3i>RjJVSi93Ui`@Zed19:0E`c^,!Qp]5gUo*#"%P]jiStd#g0g8bUC4MfH1N!>r!Enh9>%XGp'!"mCWd5593e/)q4be(J"RkWFPO/WdNMlk?A52i-:&ruPCZi>tdqp>b-B="94cVkq2F?_((?ruL2T`qI[k6!9Mgdpu+0[X\2)gI-SmC/eGL+^&bgr%/&SC,s\O6g^A?4liO(*!Gb^2q]OMqibs-rd+H=(_CHL%QWLj'2o)@9\2B,`T].jLa_"oa?49oXQchgKgo^)IDoDhRYIbX7cm-!Ue%\]>/HlOZNNMelM<+\i?"$Fi&V'6+5k^rr,OjtN&P`1tYH>#\`hh$t?_5l37h8;8:BcU<X?]bJ*Ge;n)#PsXPN5)Z<"cQdoJR&+DN,+,(kY!e0/,_&j#,0o,KMFgdi>OGAMoY0,E>3;$k+:sE,U`r=1PMN\3"[UamZ<S_Z$'Re7&TMnQ7l[I^TlqPY,XW`P&BM3m2k*FWDtn[i#Ii&?03Mm7*%<(/kI&Tg7G8&A@%@TD<>3laD5$[2K.?&Vh\d8"dh,[5#FB^nnke,[<>!&VCG?W-1Nr'3'!,+R<M,(UF4[I65A^V4!,aO/FI9I8&e*&IWiJXj=OAMIBPe+)=rFI8OeoL$f2>TpFL.]V/T!=d<!@Dme<LU`,peqclK&:X&(`C6kucPBh&K;iY?&$CagL[+'u'sXPh+dFCt:2fa`:4<96Fk"]R[W]JqnBI;iIEU-<F++FlJ$BZj4/$S9/]iQ<Y$-@VWd2aVEbT=!tX)=+ke='i@6MSOCX`,e5+H-b47UP+WnJ>e/u?ajkg8pHbtM8o$?:Uafshhk-s,-rLn?U@cn@roa/ms5[oNMO.H/EZ$Ca+K*SC@m<5u2m$bYY1fem>gId.%TVI98u/"3o4)<Xf>niD(Qj$/0BU?#i?r3,-s_Y$P\i61;"lf3E@e;KMl`cg#Nnlg:RT?o.JE7k?5.l(U4=F$5r=S>**i\'F3C5J.2%(M>o&O]LOl;r5);!T.h'M(qW:I4Q/A2i@G5)K9Z12t9Ia=,Q(qPS\su?ioS,.Z!g^g=;=\4<-='6oPHJjO"M_gA<Y*XuB3HtLFIb3F3&[7K$YSJC2-c0"[rEatK[^ANb*e2RJ!<+<(GSCIqKR\(7;#elKd)H;HZ1?eNkNC&fU-tm@qoB=KW938-Z`e8l`i1p:Li[RNgZ:PO"k6'GnSRT]#>_5Yo!?Zetfka8='rRK9_6V6$Tm2Q;JW6[iV4?H2@L\9QL?$pgo;4'LUMiLOPJ3m3#dr*5X\$o=6'Onk(#X^l+'4?mLr/+UGP[K_BFHDdk^Zn3Y9*<bKq=l-bkeA^tumL(kg7aF(?hM@al6WQ9^Uj5Z>5UF^:4E$b:aEass-C"]IblOMBe_Q;:UYg%@.>hNPsO):f8[aTZZ:(4[irXNrfKjpR]$Q_p67[R'\R,kt'm>CP%1/ZNgk^RD-4gpjZ+75p^9;'4Jd=$so:R5sU*n-HH_h\XfEa`-HMR4L,PEY1&5Xs93(qtSP^%=C6AZ,,jpENcgY2km\`^*d'0.M!A+D".f;-t?$.%NaLifQb_Po5ZC..]=rh]0;roJXg%N^3/?k(RRK.*[*dS(Y9s+J1-A5c3!Y1bK0(.Uk7c5ZE9,RT\^#)/\XM55(QhHVaZqh@jHk[qpt@,u1&0k074Y:N)s?:&t<c/]Of]M\RrA-J?5n?gVah8c~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1397
|
||||
>>
|
||||
stream
|
||||
Gatm:gN)%<%"6`/TF[G^1\#YW>X#$!)lHE!Mq.Hr0/UA.A5T-p;YIJ)hm9sgQK=q&d7=t]3!WP:(4[NfO(a!k%FL5XqV`5V6IPe#KP$\8n*&s1m-bfj4EasC;oUl!LcK$WXJoIKEe'^:R2")4M%,Z2Yp:X%;'[>84^GgiZ+.\Agc-\OPeu't`+p,mDFk,QPV\=VP=EJVUW#=1<D;=bnSsKer+<qWh16j2bZQGQ)8Wg^<ml_]`#a.cH'cptXP9QZRuEd&p;f)7ECW]<p]sNIj+Ra[![,'F%9A^)c303j)g_M\LG*T1]NFqY>o#juJ(1;jQSn^fAON`YT"V/(<E8oHqT7XkXOqE<?@PcB)k+MIVC2:FRLl1qrcc)NR*KoVlHYa^QM+go=QKk"$-st=e3-=(OURA2E55S)G3K%&3k<Hs6CZPjAudN#Vh*Q`ehJsa.0pf*6nL1J5gW!.n+"CX2p.T;aWbY3K.)ir8>^r/+9Mf]5leE?Aj(,Fpk[u?B,Wm_%Ni(3#B.7MBh<fgTR?IWk?%D$p.41$*q^#8aZeO"#>"JJOdW5:+epWM*/J)V6tD(3lp-:I"7>X$UV)4OPGSs62O@8N=0Xtu/3)1Ol4bIt40:j;9INl5GlFs!&0\gAbF<5.LfiRf7-]9,J(S?:P!*.ld.7Inmn0,*'rjc6QQ`Fk&p3nr^_=!Aa^S9!G#p80iVbY5qJrooB15Qs5%",da7oTjI_7<(\h"E_P)=%dnT8bj:2Bl*OQp<BF';RsJ9_ibm"AHu(Jj#9B>6#(!N6TtgEK-A*fn7g=FQkfCI0#@)S\;\*KDC::;rN<=NfMH98aHtaluCBj1R)^T$egtc;nXb4hg_>Z22IIQFk[.Cm:q\WHT'lS&#J#U\=?Kb3(DapEr?37Jc\[XGZS)K>6Dmo+`j^.:&FJa?U+2Y:ED4"iH*!\VH806F\Wd!B1:$l+Zq)`rq"7>uTmVN'jKY!9qi<@+\"?ZBo^c+k$>/&_[Y+<kGjliM%,L!faXd,&'Lkl0Mkg\pY!A6e@[<e\84,FMlBF+0DrLd8Wbbgeb%%H4(BbC"ML9#0-c&W'$V3S8Z?`Lp],N/[m+J_q<BT)Gfoh52E_1-UtQ$O5j[)^H%GD7h6%M-^s%7>QOF6njo?K5bpIF/2uf,dd3ZI&h0,**+>'Dj5E/fU>UucA3KQmQmO%6.%D-HChGQX:Dd`TR#XrZMOfp*Co=1]l!n`>$@ruDNASe:r:llcXMXYs3)u?pH4UTt+g5^G<LlbVi/P4,N(p4J)`8XA<'RsAI?P1cqV0/m`n:k)8,*U(e!/E:hIfgTdM,&($L'J?.&8/+d,?U8h4;m,UYem`d<Y8jBmdNmrK9LZ%52YC,[#e@]>R>(X0Zr3-V;>Ge_O$GLM$PeilCIK~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 25
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000122 00000 n
|
||||
0000000229 00000 n
|
||||
0000000341 00000 n
|
||||
0000000546 00000 n
|
||||
0000000651 00000 n
|
||||
0000000856 00000 n
|
||||
0000000966 00000 n
|
||||
0000001171 00000 n
|
||||
0000001376 00000 n
|
||||
0000001582 00000 n
|
||||
0000001788 00000 n
|
||||
0000001994 00000 n
|
||||
0000002200 00000 n
|
||||
0000002270 00000 n
|
||||
0000002551 00000 n
|
||||
0000002657 00000 n
|
||||
0000004898 00000 n
|
||||
0000007399 00000 n
|
||||
0000009806 00000 n
|
||||
0000011432 00000 n
|
||||
0000013268 00000 n
|
||||
0000014876 00000 n
|
||||
0000017247 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<559beb0076a70a7b1c05fb001e503b20><559beb0076a70a7b1c05fb001e503b20>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 15 0 R
|
||||
/Root 14 0 R
|
||||
/Size 25
|
||||
>>
|
||||
startxref
|
||||
18736
|
||||
%%EOF
|
||||
@@ -1,289 +0,0 @@
|
||||
%PDF-1.4
|
||||
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R /F5 8 0 R /F6 11 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
|
||||
<<
|
||||
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/BaseFont /Courier-Bold /Encoding /WinAnsiEncoding /Name /F5 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F6 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
14 0 obj
|
||||
<<
|
||||
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
15 0 obj
|
||||
<<
|
||||
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
16 0 obj
|
||||
<<
|
||||
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
17 0 obj
|
||||
<<
|
||||
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
18 0 obj
|
||||
<<
|
||||
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
19 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 21 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
20 0 obj
|
||||
<<
|
||||
/Author (\(anonymous\)) /CreationDate (D:20260422072251+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260422072251+02'00') /Producer (ReportLab PDF Library - \(opensource\))
|
||||
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
21 0 obj
|
||||
<<
|
||||
/Count 11 /Kids [ 5 0 R 7 0 R 9 0 R 10 0 R 12 0 R 13 0 R 14 0 R 15 0 R 16 0 R 17 0 R
|
||||
18 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
22 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1846
|
||||
>>
|
||||
stream
|
||||
Gatm;>F4&%&:WeDbY)t7Bqe/L`HIiah/0`;2ciOrH;7e*d=N0ALb5:AYO>"?`dl1Z*8!EqZ8JCQo?DYm(f:-?mQ8\oIN-+]<t(H$UH;k8#T2C7n"[_N!T>jDVuX]2$5<^XE9W&Xb3:$=j+)j%A"E`>9&fYEJ`dX,qojgn_qicB#oe5`arp1moU&V:em=G7;$Dnu_?1[++J*,;>uB>]B[/t15IGoAgOK-25U<GYM,-^9'GDT,TcR8VA0=#t!t@?d;A]n4;[2rW+]6K%M88Ll!(7+2Lc\1c2jd$i#-<Y\]*h[cO;?4lK8[5-5/2iA@]HJR<YR!118U"D#/5>R&!sJl?WL0))*NOd^!^W"aE2<k^uuh/:[Q^Rs#^\r[>9&)hs(P8pe0dq;GE$nC$f^!n"XT[8&.1B4s4,_>h-X_OUpV-c?:"?cD)(j(OR5c)JesQ-5rd#)2Ja?BsM\W;2hQ;S4LIV:=AMtMOdm8K]`XQ,"-1B%iB^<AGm(C@jJi%K'Rhlfk]:[;<ocV];736^RmhVGbjrQ!MThiddFXHoGf6$1/[5E-J%9?FntI5ftN<'LRD9M";en'/tdR7cAPaFGV5DoX-\Pm8'f_kXGRX)f*f)`<dKNJbJT*I4QM+uVsk;JHu.g-pt,_>DU71X\=MZZ[M*(PTUR*Dhi0oX:q'6u@j^]X68B[T_N,3qfg9PD&7$X$cS0o9ENpY#XaStI:j,?n!o/RS"*-e(%^Uo@C#I(s/#(?e%[U*U]nsm:<u%+>^f<,:n56A%]mc?W9q<`ULp41i?Y>i31/R@iYBtdsaUg4k(9#J-7dZa.+O!p(B*Vp,Ko=:)r2#$ILT`VG3'hYH*'c!&<da?OWD4d354(^_6oRn,.Hjo3?5G=cV[qc8a\IB(r3O6X@2,@i;;B@^>Q(`KpD#CT$qB+iH5N!j_VfR$V!A.Vo4g74(paJMJn63gAu`XIY*oOhfX4idIi`IgfS\G_+P9_UL.;gYgsti?T#ATk_%/A!21,2USn*7Gf%!$5+UNB`KiD+U);f.7N;J8a'l4mF6+:(N8W4/GQu3nu$/Jc_k;.9e_CB<,WYqa&rICVsgjW)X6qG#&rR_B;.'8fhg&Dab$n\5;\]SU\1YI)TFZCmmWq1jR[4-@j2M.>TOQu9qLD=Rk,VdXqn9gYA]rpt^/cW\8+$LttePjt.Qk^MrNcAK-"<sbmCL,4_]J!m)?GD.=Y=uKc9iC$er.=cNUissY_p^87p7+&q3Hf+@:aC3iqF.qgFk'E/`T"Kf"sIN%MTd,BA&-,'$T+L6D>aFso$&cAPsoSr#a^j()no,<e8K.fRHee&9]g"dAF<QD3\j?MB/D/Y<Gk0'']gSq(_ml(bI-#E<d-%V/]erBec`9iC6Ib(#q"'>lXWff[pH61GWDn>QpNI#WnOt^^1#X/@jEJFkQ$rD)eQS1XuDZL2^4GHItm_K;ZW>Oao0B=5^*sSa\<BmG?L6ij"-WXY"i`/S7At"Xr`iV:1_$;?OVtWB^^i[-9$,Pik$"ZfD2LM:*.t\0BhiWEQlqYM*3(H3Hp+T[ei\VhGe;GF:4d_Q-^2n6eKOG05Rg&p.&=Rn/-oO,Y]6%Qmk*3L?3>2Xsh&UJ/[<Vc\e1cTt\CT`L[ETU)]r>I4tL5m'W#kb[qgYWLiA9oV?_QG$a7(&,B7.*+79n$DgbcF'F2/HtEQC,e3W]qlFaJ+5Dlu4bh-phLWC7bc3p671lO*XNKb&.'rEe`MiW@GI5e,h,-^j<+1]`(.EK]@fOYpkkA_3qqD9!4[O"oP2X4&f27tRN@fCVf=cKXLfp=[$A1Od_bIVMC:]+OMbVP6*ou&1CE)IO>M<M]@IPA*cA)2~>endstream
|
||||
endobj
|
||||
23 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 3094
|
||||
>>
|
||||
stream
|
||||
Gb".=bAu>so'W^FR]>&']f$=]!"%enj\.GiA@%Jf7%7G/Td`C(7?JLOZ7#JEm0<@8_m1$EBt$tM#m1&j^UqI9iBM5ZNu%dLa)Lh^#O]J($ZP&1(r(^Tj-)]'*05f2$#i#k/)LG!H.!-*'[r6SJJA\9Qb,H&:c`J:I;iK4%g!e;U#F2JM%eJ0!L$-dpiO(=_[^!4U!J,EgiGS('aVjF4JWW+?6Qfp!ogKSTnWO9l\QkF!KdN/;81?[8FA1`o3GIKFR]hlm!VOcj%4Yl7@+E'^V6HfO;q3W#_>)^-->WY-Op`=f90mJGeTpuEAV";kdU;YhS=7teR^kdo+_10'fqpdE9"M1>eR\X.a+WhoE8LbH_'<BL!H\s(uN1)N(E^4HXZ+licp"RSLu(=J*JRXb"3YX#i6d*4dm^#"E"b)+/l?.>n7+aJe/!DpcRN1RW4gbD0n(6E+TgP[d)uW_nZdIhJ+;=<HO^F<(U=\S0glC]8n<Ti-C?Dlj@!ni!"?/Y%ZdJolmbIjgj1c)<hngFP?WC+R*2jNjiA)`7HWi%:X.hP1Kai"AA:bqL&6Lk"[YTi:@gs-U6m4Z>o=rma/5m]Nhg^et/M8!?Y-X_Nh:S".k0gRq*Y_8hPV0.i?TTND0ZAA%nc9N$g==NJ1UG^)MDBMts,s2FqCUHVDd6n*n+/!Jg@23^\A6&3!qXRQrX>eeMFaa`ak_,1C*`YhEr!"Ff`Uk`UrKG=4?t*q2UBi6O:FKGYnp+"iBX.rG.RA`F]bF^XiFB+X6i,b%,2GYUk5BO/D!b!=G</?3UZaoT'hDULK9ogM'l.3>Ks;FFA[_/5?Hg4G^h)R7d#N+P4=oJn.qN/WN*>ko,0LL(h!1SeVs6r?ucX&W!t*a'dhYps`$\#6gaH@iCM=uu0EW[RIj)#T^"PX/$jC!D2hN-h7RH?t+hk6Q@P45O\(W.o`I`On6%Be1%#/;`Zp=b8"pS1?>Ll$6u.LqQ7b<]%.VY*gI*%0`MbC/pQFRLJ^[F]q(qVRkfi22b1GF4Jb5;m!?J'CH`V$]J*"d\s-56_7"oJR%MW2uP*"Ma6pumgP5n"@k`^-p5fY7tIWfO?sRAd^.j$Mc-QZ;f(_W4__^cAUp0a"%A+R[F^P6"Tk0+g*!6rlQDV3)&n-'phNXX#;]V7=84Qk2%3j%ZAs3IlnW3EMX;9<^SXcq=b(MG#mEgN[X#TA_Co,D#)GG8k`L;`(?OJ[a'u1&)NdJI)'aHGNCTm.D+A"9n:CbiJ$'fAP3Zj1"V5[eWHJaqnMX`;5`r6WTM8C6&V]e$NjkS=)[p)m.^"psgWTfd@T1Us5eKiXHq<iR7FD>jMgVe2i9tVtDS<ocF)e8(.RH")%Xc.?K]/k<C,F=\%_-uf)J7)3#%1C7Kge,;?QVetrN-9_ks]XeV#i#&U&!-@hX]?+@CJ3NYoc>W@k;]q.nthp>l8/S/P,OnSR()e/,F>Z#>@p\V)9SY9j_($<4Z1>AXo3jKu!L[-0<5F*-:.=FESq\;.ih0;.3J-amtOkC!5P]kUMVcZF]t,mkD3,S3EZ**Zd[cs.Zm5=)Iq6Y(#3-"AIp/esRl;$"`<mJ7Z_%H^N4,Bsa>m:2doH5/%U(3Tqr9DGHgsW8fRiHC?4[TjE"u);Wk./T7Jg.cc&Jh7pqD]-h^"2XrAFq4%iAPh+Ni[hJQ(#,\Z^#-R<Ii`9DcA2Na5n^FY]Pa.>Ed$dD0s'3Q($Buu<6mt!ik*dNV+G@n"dT+p`ea'HB5N6*S6:D<(I%eEg5iBn]7NeMmG_%A,,4Wdq*bkLb#udVe2H60GAZaO`l*68;L$D33UtZ4/Zb3!Y,8Jg!PUW6/<Td'Pc#I:N/Cu1[Brk7AH=^&Keq@3i]5-`QbJK^)%U6ikG-UH`XG&<u&=(#:cP!1Yk.hQYDqbc9bOaipU>H?k*pFoVGd!RCDAjL8G\XZbAELjs<XnZH]^+NE]4#D_ZR5V[TR%+mXE&ELIh"hDk]2qh&Ot4H[a`:piL"5gQpj`Ee!JXa%Ci1j0G&]VUU5\9/9pA=Z,8*Yrk@KpVYYjELoI9HM2`\?j)^1Q,VYI"7%KpOR&9^\YDRdt-@&Q[m&c't.F`JKFG[`RijPi;g!(sCQm[*!)mW]$S5V@\mMGO'\15PQ=^rNec!d25&bX&&[$g<CO3=-6m?rO!pCBWPnepo*H1cQa?2K&"(8C9CL^FfclHi)QiKqas]Hlf4?'6%CJ=%cli:,tHH:b&ga2NB%-%!,3^r8*B=e)R8Mtcd"mrb8`]e=LHAQ(.8k:\a\\3G7PL'qYpjP/k;&u^7*;o/46h\/!=(<ueN*j;Y"k=_`p[-h)u;nfnpBo_o#,t]HK:!Jd%Rus]&Dbc/_/f^1@/`<*F]5qBBL)sNNgkb"85hJLgqJI/LIj"YR3Y61Y1AJ#'nE/6Pc?d+Umpe!JJU@Lt"s<#"\tttQD;<!Ng^n7X>u_4BKD*)!PFab0<-XT-r<VFlC:U1F;#@*pTlj&`Nd'=Sd-EFT'rtkOs(6?48PXaQ1jKQO$#u]o;]?!2:/g8#p,Y"cj!<l0=YgK+<(V=W14bB>g@bR=?r1g!Lra3"ip.@4Bm`PemKjXEd#J_mTr)n@Vaam7K7sp)'f:tDT.mFe9"1^Yj,@cSY=hj9@/dDmOU'GW`1GW#Ca*dq*:'M52+Z*:?^np\r3kiQn==0q8p)@\1K\LE(a4eH[Ieg?8q1HcA'Bfq>E8'<Z@;NReIS9:IiP4IG:Jb3$rTA>&&N^bCK"N-K)$KMBt0$\oFppA,X.oKghNBTSh`HWmV$;rafG%WqH@4d]4ZPod%3n7G.]lGeat%QmqTC,^V0\!EHWk5&`eN;*):,0pF443<!rS?</4"W0o9`uX`QZ-N)]>;HA(W+)j*N!Ir"na%F_I\<IPN[8:7;d'u*0FcR$/&$e#f:^DHB,=i?Y>B:!7+<W;[/IZlt<N!t';AY,TKb>/'([l,t.@f-$lA/M\";&EA]k$VV2,j^r_h(D[5m09_U!pqo7p#^g(0F[$[+d`T@1GB0[FVa3&K\eu:7.,@Fe\JV7RKgAE(tkFtD#V8+ljoSM5F=;;+a\KNYqqH_[BYkbf;;$l\/AgJ.n)Gn%l5&I9E~>endstream
|
||||
endobj
|
||||
24 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2820
|
||||
>>
|
||||
stream
|
||||
GauHND/\/g')nJ00gN!/)Uo0aML&E5EQE=PDXZ<7m`*l5"8YHK)+TN7BdGcRp=J11WAcoIlt!dudE7\50-&8"1IK^oNdi&?DnH)FE=/W4*,l$SYJG4H$e(/U5!Ac&Le/Wlg]]$i(O-Qp+I*4<.-F)<&dM1QKHiTUTjkl?*XKSEhfjGr2-canZ*9qr\I&Ch:Z3]Tm-.dkHJAVCO\>s/!1+#RKJ^Tp#=pI75X(Jt[.M,H5@Os@OGV:di-C=jb$eOG:V[$r^btV#n1KF<ULobl/M^jCZUX)O3$&'@?.9H4.?6&$dLZ_T`_r?Al*3L97ahM\?i9fm#P^<OlRKWjo+_1&QTDj[iJS72S:-cp<E2rIH3QUuH_'<<6>,t,R,5^PKL?@Gb:mIs1g]/@;-+=G^N>bfeLZcJ6uSLn'$ZUk!]f34.rNrPFhLU,[&8u>N1[ksdRXRbFHpStE*Pc#-3Y=87B-O$DTJ%m,n<gld<<6E'b4Ipm<a3o"@57q+moSK]<iZ*%QXeT<6#36VrNiQOcUVubL7]8/J0@B0hK@k]UdJ5\C!3h)u/k.A4PPAD"_C.45?-DBbd0EebC$e!''?[3EYDH#A[8lLuB[i6l.ND"sC<hD*B4b]"!s2^SgNgb<Zbrj6:'I=*A\qV[iHhmt^0sS:AgA6uedQlV_BqfIB5kS5D8\7)\7N0<_`XB5?;5TgKkglStRQ^`;6QI0\>qqh[G)@mE^[rPqf7R-ce;/+tE1bH]sRm>BjpP6bb=N4@>\&jm?>Nb=.LAQ%#LSi'kY?H7R^G?s?6]nkrWQTNI\4jQ*JB<C3q=ske_#qD4a1U`[i(,mGC(u1HT1p>k-GA)*rVbXE=0/fd&:],RO#8!Al=nGRD,=:2p&DG5#dXLXS-Mlu=n:b#Qq:lIkYMFRi,jq&/oD*pFq=r_K3+uYm":.>F#UtbkNt<PP'20heJ7s\c6<CgOB*<XQH#=0sFE*c,%BZZ6P<M<-b<8&#lts`oGTnj#rc?;XP`,ai25<%J/8=^]Gq9ks7V1F+b7-o[0rZ<8DU2cBGW0Ba$VngJeRZiY]%<K2*G)!Ba',R(=&MSejndsT/p<5,V=ELsnDP_`ILe![\rNT^C@q=.Rkt5d\>)aGgcRk+h1YaEeEnq`ROIi'PlC.<e&T/;TpICm5l)rCTIC;<W?0=JbrD;cVg%H77Yica1!ASoXq54jbdXI0W*uB8=P)Hi%"=%F;&qs9X=B2Ua+MD0SD<,Mq^@)"R<4;R\AG7;(UmN4HoYM3@ksH7e`+d<n&LB_,^KP`.0`8q>524XE3\[@h"@cW8MhW#"7;I+m!C9u""F)OBDeq2kkdd!%2$OTirqRj&]e8]%:Wj5ca77W"5Zss_is*21U,6OZ4Ci[hGF85Aq+7ffi0n"UK6=[NGO$ljm$.E%D&7KZJ,/);_bI?\)_L>.r`N`mf*Vr]I^+r4=uo!Y"jm1^Vs2prjOVQUMr/*KEoqf^8^U&Skk$o^`_59X<6+qFY:*sOE<FH$r*)q)eL*d-4+,q0TYS&8/k5YSr'a=I:3"t]BU5IBB`Z2\Z_\]8?(c5Xl(7Y/GPTCGFtXA>)t-cG[mGL3pgXTH(&)?gI%!3EUQ.^MUI:DYrXCmoPkBdfbq)4/b*1>Cd[bX1SZRh,6bK5GU^@d)`dnkr"V]?Gj^.$]^!23kr=HI33%Vjhdi_744I/8_%bXI\?)J#DH*DkR('kbr`ZY_MJ@@XU?M.:>J;*#s.fWZ)!V.pR[45?W&qg8S7(bejKm6Y#nQ-(-pL>PLBLQ-gWN4tCJfs<XK!Ak,b'iD&KJs.-L`1J/G(,>+2JXAGu:Ec*6XH<kN<ZDeQ#T50<`F'nIFZ8UY`-`ja$(VDtE<:]>-gqL9Wk2X1^s*+t(9W'@&DNO#IX5$TcP4g@lD0VTTZhb>G+SiV`-g>3HjfY=2dKW6\^YBfa_p#4S(jj.U]O:#;(IKS,F_'ah&)F;Ep!^n3Xd/>:ARE+?0*'G\B#lOfqcF$D$sd_`?rK*#8EG/E\Df_&<MXMEknUZoBO>9A%ujN,01BWC*YNMh+%pmZ#.Huu3i7tl1\e/\t3ZnBA`1^mWG#&*o6p./!hcMK`sJpOs"Lt-?d<uWp"=$u\,R8c)nI_VQ_]?-Bq0\%r6Tpl+0:K?U<\PDc;K23D6VPt>\^t_7$A;@q#PRaTKXpbt<Z)egghK@G)`H`5c]%.(Ebs<C+7]3cHNA+98?mOJ_U_Gi@VO8s/)V[KI]n%`^p/Q3gqF>P]2gJJH7ZIr94h@oDf`%HncqcXq_l_1nTQ[i5n5l+LpJgE9fk=OO'C9D-pdR4)hVFV.51F.Di(H-&:MA(^QIo0s)dDnXMGi01LFC>ZgBgnODmL-oc4et6Gf\'2NG+YJ\@i=0peI_;><^J>\(D9B>%I+S/ep]kL`Kp.G,aD=h@#tW>q=:.]2tX+mg(p.r#7:)N'9=@(4<N($=/;[!Pspl?-t\qWSMs@7_[3YYm?l_+8SpO=9*#0]f.!J4+cu6\*G=Q5".?'ein9blC#]TDOZa*J[h_=:U)f,#ghl%o]5**K[nW)h.qA?FLgqcretUMl/hC$8XkFl?6QCEd2A9^Q0@Or\hjEO*5ED'ol4!Fg#!4aPriFZC(,([:TQkDiXMS^F#5X]KgU,R<#"C#-$<m;UbM/ZE&_JiqrIns\\/USdNo@>cf!U9=3WmcJXGDM!:N+-'ai>d#k$ESR]Dcu@);TVQS/WoX-YJJ\8qCS!4G>m+n-#&r[eC7L]8rSGPp>jfR/VJ2dJ7&pUV]Yokin'YCf[CiH<N464S,AZ^,S/[>U\YLQ1HO;A99UU]8a"g'S=MrXU1Mas-~>endstream
|
||||
endobj
|
||||
25 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2862
|
||||
>>
|
||||
stream
|
||||
Gb"/)>E@OI&q8_F0dok+)furp&p4M"J5)4a^#[U"gBK(s6Hc_f>8LA(m<!jVR@tlunpNGdqf!$gf.nf#Ku;liRGi1jn;$>U-jBD6%\A-qJ_P-&!U_]ZmbYa[];r]LQt(i$jT9LV(O-Fo^_(bR68?0rO>u/AZaoIS#R.P_iIPi%chFs\Kn@F1.i(&8^*SBH`i%(8(O#F'$U)Z8c5l$M+omt_Jql1WV@SdD#m'kOoO1JogZp@:@"V.#MZo]m>nEN/[c?&&K!95)<nQ_2i58PNbCCoW19[)jF&AGB!-;Gq&L@_k4#gr1"MD"(OlCU:&>[]Z$J4b's7%?3OAAjoc13uJhlB(4ZO/nL[L^]6]QmhLEET7q]Mr[3jNCM6eMhG+LV"oZ]#W\76Y;J#"9*4<#pK5JZ7IWKH*edlRe\UW)J9]<*k,Kr\W"++UTSN^%.3<>1?7\l5l;#;Jgc<\ZGb5\D;Z3&9Rq-?F+(do/>dCb@1akYWPrV=Bh5Dc5+ZC@r]?<XN7Lg%X:j$3*%Q9=.a#C(\Z54"<qI.C14quT<<f(u8+CuO?l_mR\2lT^@X3l1hBsbEm#9_udI+eGh_=8nMcRF?8hLI-M\Jr1Q*PqJ9G5ib[#-A]N"qRS^/W`ZNI(gg$T>6[?9<M-McOk7ZL_ISK:[#W+a=&oOuui"KV`&-/lq;>GFi[F*?XWEm.bn&E5eN6UBGG7U'dV??D@IH3$JOH@359)Mi@iZ++8tNe8W:%+1Gg3lC#'c;:dMO8[#=/dXBddDL*MfB,\mP0Pd>E@u)Z]9CT8*h`NGQV[-o(nHi`-?`S"U1[c!ee>_/N\M;uV?o`Q;4MWgtD:,F)NeD7gYe[)p6sIa`&8H!"nIb+M61_W+_mRrSdES?A\C@]dXsNuM])9;;_cq2\OOku#2;Hk'dB?p<=f";`n5a.AWj^8f^D`Yu]+3k=kj<V>4$Wr)J1[^>\"JJJ"Z7qJTU(CY1SlE5.,:E=-L[Vm3bkP!M6h39fn1%Y/r=bIQE"'F8Go!XR9CbP#1!s7WK!ueY+Wqf3`jc;(3sUfF'Ym,Zr.<=,muu:?P[AWH8emo<MJ"8X(MPS[Ak1B.Nh,FJIU(r8n"iZ8gP4iK-4BKeC[Yt(b%3hCGh#0R7j%a%ag&d.Q!#pa>dN>_e"tVBd;+g0_cf__h*OdcUu=_ONXG]WS6FPUAS'0S.YAAqqjV)PiGS71.0bH<n/'!-\U)UhfBgcljW=G%H_Fr]S3gC4jLD)Yh\ZbmJGF"*-=%J]&[V/j(KESnbDj4.nC-.eG\#5"@:Wd)!YE3XsPr!)^"N-$/Be+cR8?AFTbEs2K7ql\/OZ!=c>s#'8=Bb#D69<tqb`+1mGV_%M`unMD`X,=Tk#jl_a$Q-QlC$;)V*\cA3Et(K0desXV\tE5n3*K7.AY!q6!d&XhD)`LZZ*9)2kGEDOTWEZ+o10.J!>V9\@6Oe$e?o7NJE5R#*5:i8"!oq;.cgt#^$C\>B7qb3A?`a4phRaOKB+C4,FVnL?0ES0O_USs7gQ1IqWdKPV&4OMC5Yuo!4FcmrC;$ioXW/_r_K;KX4ahk9q"[>ot1.Q#qlu.T%%B<Y->W9@Uh)*&)<78"PZ>EQ3ku<8@IEG7-1D=lN#\m'";=RpScMjs;99`rl0%VUUUbId0Mrn`lKW@d87GHdbPKP3!R.i1]`8ca_*9`]3Mc>V2BE,r%^)C"$EYO2fp'oOqL$E\VBqJ6jl>lOeZWLnhEsj&):1<7LoXri$?dYo?/JWlWK%n]M3hmbQG'0_8>03m4I*d=TEl,X5c"LEc/8/"6tHW0n_WC]A3X)P8qGXp23sD>).$U<RQWn=-"-l[iUF,?DR8CJWt8gS_IT73(YQ2,OO25Icnks.X&J<@3p-Zad+-ipY>3^PEfs;i6nVj\4&qAsf=oIN?t<g0T.+KR`SLMMlWDSG#h^Cf2ji%?UIjY\@EHf&2g)R90B<aR<f\J4+Jn6SV&fml*m2FJ_n_,P9j44@S"lhO@"3rh5Y9\0P+J':ZcIWQ<5EaJJ7DB0t5OfPN]`Eh[67.T'Yd1!^<G->DFUULE'fRlS273[)o$LS!`+@0BNFeo%=mRFG::f>t9ZBf/m<Wb/hL:d(,R&pg8rYaT*bT!YC,Z('^N]P_UkXHp+C\R&Es.aF$HDPahra&CMXM2kf@a*t?98Fde[U7HseYfc"Y^)?F:<Ii]sMFt[<ROS^Nec_J?U2H_2cD.:-:DeicgQh0tb"\M<n*_\Z]o1EUK8HCeeHuu4s%pp+*JcOHeRVd#H5TYWY3S/@U5G(*&<Vip^*jSWWd)YDSO@^+/!Wq>/%`W)N.AJH9d/Ubf5fTn(<36;<Lf#IXu"&LW2hhua`o$DQ(:dcYsnH6f]tttNaePThEopLndNd==$)\>j&G;a8"J\YlIkSWJ?1$6\FB4(4C=ni=M0a7(csqId[i04(%-2)Y/[sQM_%)a8p#f-q*Dp.(UVA$V']fGAoYSf6XXr"Eh>;HeW/(h!]h+I!Uk(X'7J=tR;,/nA":NeF54P<rY[ZiYSu>2-S:tXR?jqls6ZX(A4&_AVDs,2/$%4#W7kH%S]nRK+A[,p6bIQ$Zg)RW$4Wumq\"OH8Xs06?KhoqCWfGkQ3o5"'&er&3B#uMbsF&p#Od,OlS(D>`;f:Tr^ZF+bVee&Gk^e;SjVu_2oYSo52>**,EQlbZ.m]Skg=_ap!.MTN/iXd=l<H:IU0U]?,3h\I_90GrG]fnOG8*J`s+JhiD1DPHrBULOmW3Q,)_h:OTV'C1PNq75&3Z)*O&n*G/Qb=NXTjn]0q"qFaYPJk?bjtq3?eE;H6fRQKSm](Tn/k'hVaZ8\WSr:NO)O8&3m`_h5Z:";hWt5;['2JZJePTb%bX~>endstream
|
||||
endobj
|
||||
26 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2101
|
||||
>>
|
||||
stream
|
||||
Gau`T=`<%S&:XAW(^)8<D/HECUG#'aA".>D8)cGB+tp=AfX,3fV<E/n?b\C]Y;DM'ko'j07&Io]$pW&-%6\\#ca3ZC)8OW"IXg[7kJQ3j][-snDusq^SSoMrE3n%'_$QhqN<=.J+S)iMoVa!33;#&Ip\Yl&/9:Y6MS]%X3(^ac1l'7e*@G(6-Z50G5!FBJMR/:ik[^q;KBED5O@C[PNZWNac"Q0:'Fut7FW1(WO[&f>j$^,e72"3Dj4F__nf66l@NT1nq7$YggNlr<=_G:gPW"pinT'[cM!Eb0S1m>M;,cQ8$]ci,!H3hE>"]DgBg`^GB$gMGn`qpB;^5W3A#2fm@PlEO"tk9fJ]$OI@9nfFn,cqf.pf5j`hdqfK*_sXOm9(AK.M(KZM;H9-bP0BhK(,7?)W-Go$-0dfLSO;dFN')n[R04`_1@dcZ9ZD"HSsGdnA[Sn_hINc+U\"KEs1K@%G02hrkgf!S0E"n>Tq2r_`oTK$1.0s1h*7)&dV!AAL.ao[RnG>dTl'ETt8#0nDZ2K*PV%VXnd<&=V%_PpY`:E4O[rhfW07>moJ.!SnW'W.]5Ko3'VY;G-]J(K=q#O>(0j>,VViGg/qRFuksDi@s=#Y,<[>63Z+!FiY00oi\e*&_]<Go*$%Z"4a%')Q2OIi#4MHqVVMmd_nIuY4V@l</Yq%!JZZ)A.V8cVs2p]NK3.sm%XJ?GW1N?V4I#$gg[/j(>gatoi_gs;^Vk20W'8$75n:`S>gKN]B[h@g0oX%/#1;N-W]99(Npbls&e6RP"S?^n\cOl1NUaNc`Jnt_'?h\59u2#@rZ4S&(n)qaYSTfb"I\!VDjWcG:Y3559?-,6Xi(N?;XV"[$k!l%F69UjLe,F59/&BKtn\[jHUe10`PF!B',H`_.GMeccpj?mf@['4<i_B6NV^M;`,ln<k)M?'"Y2,f8<AKO*,#45A6m?!$,U2#^#^#5lZ\K=n=/[(TfrpE:hd;DiURt'rumoZ<0rWOK&keSh9Y<QIQ`%6[HXe_63Q]0_ZWp^r6h@q>4&`iS\B%b2V>Sagm64bIc,f987P#FWN4Qk"9WA+/m*L0oKH1OAMQQf*4)ke\'GK?R]]r.QOe97!QW^D$6)=*9;=DLb!3W-5"\Xa9)81$":E:n@uLYHA7l*$MVaDVQN5Y/*_>c"FiA#h`BbQD6rTphiO[C,,"[N7/U:6o=!-g.j4f)(*d#tF/:`heC5q[U%sQ^X0$L2asFJmg>Ke\JLqXESgL=Y@C6K\2)Bj9e4?ZIU-r9qJ8reNhr_R0[0)ZZL1D9,M/<M*k,,W=[C0AjFp=.nca^\V9+WB*hN%7Zp+&MUb@6rXZtPMP-k,Srr%7;3CjuPA%85E%NW>E?_JVA'W>UBAAbp;3i1S!heiE)sadCFnhI@4k#6&4n0u*An]B+'KLcIQdicMMg:JSW%6%^-nTP(f=Qj2tJd^<lQ2S#&/>0^k(Y-g<G0q:H1fm1GGEGK3IBCUZtT?DQcCXL;_5)l#sKfc1qPb<K43SH=ZK#D96)p__3Z\k+MdUg3NamXIHXO'k.Ti+hS?]h0UoK*La0UHSf>oiP9XFLs-VdJ,i2!n%/jsB8`b_)I$ic%\.hCa*CMk0,WG>IN#E4!Qp#$'2"YntPF?VXCC?n_tQPW0'jeN<.)ik=#d+so1q<4%:C-""8N9m7N:)`c@Nf(ncLhB)]-P.rZbm)*%D<[\8!63IHsFJhZ.L('hgaGLS50:qSg`SHjkDP,MV(?_DBA18epT0aA<8%Xc&p>r6$30sL\T.0^Z$>*E#5,6X_Z&2sqU(<8-A!-E+@[CNf^0GtsTZ6kLe,dn9;BWM?gdTKLc?P?BkDLGW5K9,@QuQr4*b&S@`DGL(#M=n)ZWrCqli2KlEe3<n?CdYSZ7OY#CS#O*bQ8[BXAT\A6J]"71H"0gQqV^,iU=r=G:2X'H.Y,1Vs?7dp=7+;kVa0;F^c'%qU5!oo5&!Z?5O?rO'3F,kI^$dhsP_VmohbERtr6Bg;40mNiHCsR;9;HHEkEVe6&oK!5?n*VRhCKlY^&N>WhE$A`H6P<J>&+7%7CnA;d(gDATmm9s]8eRSYo/jc]:E@OYtrMY^@FLX*ZH%N:]~>endstream
|
||||
endobj
|
||||
27 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2882
|
||||
>>
|
||||
stream
|
||||
Gb".=flGh,o%d[#jf7"^UgaH,!?S5oRWUBTREQptXL60.?RQ8IBMkRkV!MX,IMs>R8^q=Po!2K!L]i:Cd!(r[`IH,07SA7j@sI[,!TPlJ""&hSnH\jPpu4LS`1_`X"V=28^?Ydh*Q\Baq<T:o@%RUU1[]i',iP>:JTKgs*qLLUE;/@D";]jhde>ac3Kn@-_@%!de-3i3iqUq,6+CHV5#K/n#jqhGGUiK]K)aFu(IZ+b,4R)O?'bkZN)8rP*0q^-b!:U)[fgS$XH^F71*E`bOU*4&I3:j!O%;i?r4k.E#f,e$2,kQ^pel/i?Gbp8h`nf0T<jq8U3W%NnF&,6dCb-:c98[dIs#$2ED`_br2:Z9P-($"/\d)24cE`]dN/6\&.kj%mc"2=$4,QkQ)8OUc(_V#YE8brcJgY`[tF'D<*-u?K,7:r!'H#2*9!W\ELWEa8i!/\C)"r4FgIZJi.pbJ$40!9:(i71M+WSeG_Oc;ie\s@%WRV:JVsXN%n0m??9!N7Nn3,M]Nla,m@o/,3o=ZtmE=)Nk"CC&RROMNN:p@o)BaoF$3&ZMp-F[3&^95*$V2qb;sABsmK;n(Z<G"J2/eaC+0>P[:iV([?KhJd_PU%*&DB`_]gEBjkAk(]*r;6S[63IuN+?cK=2"(B?F7]LBqI7\kuj);]&\3;>tpTR(nkY"j%tDSY41)p1S;MIHD^*'@l@?V2n1IUk-\j46_&D(FZFP:,.'ue.t\)^J2RWdAF8Ggj74mSR-W4)d-mn<WJ)8(?0=YB_d'1Q'$<_@@#5:.J<#.(7.[W.n?XV'607>*E<:+\7D)m)jRng<>6.,!Vo.jrrt<>dKEqhb36&]97PdgIWHjh*oc:gB^>H*$H+09'1gnSV_dJJ4ef\E=B,VD/J\^E9e;3dL[ZK"sN/["8&#rE*%r/V'Q3S:fC5/q%Zu4?[^H$qUfUe5,8Mr-nd+(]1(ok5?'c%-j[6Ta:KG_huUOC3pokc^Y7`m.USJU3iF1T\_S_B>\HH0,slT%$9Mu/)C_g35P@;fu!<*)*piZnaJVYE/&@@EnabZ4JV:8s[t6Ns&LPR"<hGGou5<*mF&/mlLa'kNb-<Jk^U.UT.HA^/5n<;@0uC')jH$tJ(D1fip4["C>87<<8_,1L%\q"uu*R6/LeF]pt@=["D\9G42Mf342&c!J7S*mRqY7M?NjUO0Mg?m3gja#$dG5=1(LM1Krm27ujr\]uo<:GUE*W[:ek@8piaSDmOBf!FK[mc#mG^I1M#@#jApI/e/E^<X[\>Fjt3Xs;Vbjs/$s8?GK@\4#FaCC>4-*[QQ\of>UD9O]Ohe(J'`\`ZfFbrj1mBO)\<I08`ki2eWGHKS[aH6E*G_hfPdhU;aJ8!rpj,/*sSYZf>j"J!cY0DPSHL`WA78]sl/A\teo2Xtm4E)ggaL?WMnBfE70d!+rG!h`[q=]t37XF&1)kl#*jn"7``_crkLam_J)FS!E^88*Z?OcoTD*ntgFMh@s81[#0u&m+jZc/,O.=9Ls2icn_:7&#U/%dE9+&CugXYGp)!`C;"]l-Yf#"kAd9O:Dr(cE`m*LK%e7*)kU"/&j!G[.ZI7DjHkO<mfQq>\TMh6I-9Z28-9O?aEl^>&6&<_q2JpL6`<-g8OqO)S.OoL#3q2f_49XoX3YiP]b/IgJYF[h\B"EY(VVR@BSL^OgN(\WTipuD7_m/)6\Ui8S6:_kk5gPP'/?MH>4+krK7XY0,@O#8(DNWn[[pMp;O73laS?a265JjDW`d,bE'65;E:Tn9L%0ukSa]Tbn$.K:Q*I]kE\bb$^9D![e(haq,oX:$]#NEe0_Lb)0<&r;P:jUHn-W7:&%^o21,Da7"+!H<j5/ZNa^EnN/lpu!=r:-_%C7'b5q'Pcln%lK'98+*MrI6k'?Vg[tZ[7$+j,A>*S;F#tq*!9O`h_nINu6UGtmS!hWHk(uH-kL1(Rh7]@osBrpr^d)_Veds+o9^Y>dAQC"jp,4B/uV-gK*;1'1\"W?k2/FmG("Z!#j>f%]0<`!Dn;Rbg&Kr$q+_\=`N2=(YR?EGi@OO4B)D^+3H/Yfba.5jcl$EbUZ35Qkr(Zgas2c3\:,dbaiZksk8VODl=nT'=P?3.Y&b[ti/*[@O0^PS?Hnb051W9,Q/<QUf&WXMF3gi&=R3(7D4kpj%d14e>!@od^UnFsLm1ZOlJAApgmZgE-B%o+/+5nCF(5<&PuS=;F#ok2NDO%hlWPCUk2[2Q3.IT4tMSiA:_JPSDc'0@uqGM<3]b*"8IW/rXG5G%*./]Tp9c?(Tq@<eb-Na?Q-"67piD:^0)-GHs!<MeII!`hn&cs[NkX$"`!hE^cZP,PR@C<f<_KTEG#.T'P(K"7=`Gg/eL[4'Q\/E7qC/"J-PRV@u^Nee`]Yg+OYYKXL2,/OF/kaN&\LF:3XgYlRR^nFcI?<=D=f3JZk^[sGi`24BTRls]0$hJ0>`24So+K<0p)61fb#/S"$HcI7RG[PlkVJ#NTB(%1)O5TdA'4uFaOS>VpU>=96`j*98IhR&.Ksr']KA0-<fG^e!:6lcGg0AE'nAYb3R]U%!eTN9-*$Oe(BYREpXRT&H[UGG6B^fbNWc21?mhtKf-dEcg\_mVicp?(i[15iZ2R6>@KKl?@.q/F"Q'HDjZr?PsU#fF[hPmL3`F$i]'b6.MB#5)2Ga,eb`Xk8]4ibD'q]Jt<cC]m&&d3["ba0ie_!*m8g6h;PIe:E`-Wark.Tp+YR"i`7U1Ob9Cree8R(N'IW2uD]<cPjc7;9*iomj[j7eT(m4Z'5$<)f2pB!M#\=hpr\@PO9gb[P]%m`o9aWi>T02a)@tZ*H/+D%O'A_jX+R^9"=;C+BAaoQnDN4P^s*p7XRu`H"asiGFT1CTREDlI@N;Xhr%aAD^:F~>endstream
|
||||
endobj
|
||||
28 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2192
|
||||
>>
|
||||
stream
|
||||
GauHLD,]1K&H88.Z#_^l8NT@3Z>@^33r40pq?gs6KeLF]Neq+`[Xbg$r`53L3.ZakQapNi8Cm2m>4&Y"M#kR;B)a2(^p=mu^1+l;bV"GQ,%1*[?Hsnt9Sc1`8Hf[O%qOT!Oe0lD!DPJO:ft#ZE5i"L,G4JP%tJ^6KHU_s;/l>A6D@4=UqW'5>L*-h>0.-b"Z-5!4Q8q>Jfri]OdY1h-&R$]TH5I&3Vh.U'B6n].L@Pd6,3WuN)8pG*(o]\Y(9';Yl`2th(#7(A42#eNC9F".Y.kV(t"LBEI\p[.)7l>%9C,l8DbPYPq[XdiQDQ*<G/=+k&jtFBAKLsY2PoB&<9NrNnZI1[7]D3\kto0/6/Mg_s@_B:Q4P#IP6Qa22KlV^JujRn@,Et]M7_Qg;lA;+6+oU@L,KX52g\@JK*$fTKbg!B)MSj6D57[pnCUKi']q1j@aTh5cGBLLkm]H%m'q#oNgYg+$7@6gc1<b.9Couaf#kTWP0`70JK.(2*P=1a8]#`1e"Us2o'b`C6U$A_$RK@dP]OK9jeH<T0P8ZUoO(4MB3%-J$c'TmLQQ@cqpH5,T$oA+kA[tS9L#*Z3L/5YoKL(6[W9<^EjLTk&t/a_?M?5B#bEhnQpi\1ec6dEiF"l(Y@?o<f]0Y#tQalr80:!]IoP)qIV=pq=K@rmCPDZkDqP1;QC,3Nn;2nA+Eq_JHJ9dPp0bCfkuA]f&6c8N\_@A8>AM(;AE`BacU,trVRVie9,hnA)?/4eP@G$];t%.?a46!70T1g[gN"\kh1btqbG2(47QZH_9aQsJM?pHO%fO^-mJj3](fM3G?kli*Hah>17Y*b&2J97c.r?4XRJ7?65-u%p1/3P=u^Z+`?ljh@equcOINeh[fO^0G(X(!Fs7r;g4Q4[3ES(k'c[%[gmU)](hS;e/DMK'ZA`4:5Vkt)4rhdao[e-8J9#^NSJJ]SdF-Th@;QZIHdFco"/A/r:>dhiZ?6^M6CAp5Eb[HTAAa(mVj80emoaCZQ<IXW3YtC'kn0]uFT$Kq<6b$E<<p.!bmFe"6Wp=m^q`F'bVt7rVp3<E*Mb4(0KlBDNWP9S\Peb4ApF^S\4XqHkF#nq8PPU6p\_8D9q>T<8i;F]@Qf,#iDGFYVE3#DBJnu%/sD979Hgmr0(VE1k+JZZ4GcYEkijYD>.C]('TL[:4Pku5P7YZ$$\'u_:@oWq.Z=.(22SU-paMGIg`SMQ06K'H?QMG(R@2Y2Mjq8_EN7aY6H?>p=oj2D]Lk%\\^E'TGROFH\GS[fFdln$JN7qaNTu],ZG@bOn$;A.)"un0E<'f,/HT!W?sSD!13?dkO>rU19kXn%`j'?E*aeRB9<eC?d\[k(&\#d^\PgAn)hSIG8PjtPMdE[Qd$5la3_b3Hq6-FP)JW,6:gu-E?eB2lC;D%X!?[]#Wu$LhoV$R3-()eqG&*qp][2s!C[K]Tog![^CDBX;?"ZZHN'WjbqG=5ZQ;bkjVm'k*X3>t7bY[=D;laY[$lX5]g_78CL-,b+7t+ISXX`8'Xqt[)/6XHeL,;s9k&)l(fFo+SBuV.-HSKT=A'>:I?:rQkXHkK426&^k$,?rB"Fq)sPlQF"dd=Wl[E3q`l;>RUM2LaP:cX)j"Y:">71<!dN.&E/5]@=5RHcosU0(DW@F9KlW(5d(C2RO_4-/fUC&lp-)4Aoq?)>0+7JdEn6/\eY(95HZ(.XklI>Z&oH47QrgClWBOo(%$S`C+N'tPKe<&HmT[D::B2hURD2_[Z%?G^o,a59s[,'@4m$DS,Qf)5ENeoTANfi$^8s#/G!-uD^V4>0Y?Mn$'LD&4sG?ZCtqq,47%P.GDo64kbbYd1U*=VVgWg+ajFc%2MS-5$9-''m7Un.]fa2fXP0]25WLnduNbr7O!M@?Fn:VQl/<DV[R0F\Z6$jg%%fK("tI%Y">!#Nr"%"W3Qu`+Md#WRlpW,/0N8E/fO[T=+QL%&bc8='$5?,PD"CmJ01e>bkVYe>ok#0B)-j-#f3k:8`Gf$2`^QM#UEn@IY4,iA%V5L9gOBJt:etM<I)=%Ml0R3;?9T3'L;f?=0U#pY68&\T_W[@36?U!hf(TjB`g"EK7;h@EGn+=?9_hB'()$DSOuF7@os#aW.M_>KTe7cW.7"B"g@FT"TN"HLg4YbP*o3+>q=^9=,I'Z&;\@Kq3"_50BC=#fs_)@[q<B;OI@i+_)PZ~>endstream
|
||||
endobj
|
||||
29 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2524
|
||||
>>
|
||||
stream
|
||||
Gb"/(=``=e&q8H9kXnj`(md+s?uY6SYnK4b,aD#]pVed.0VjF+!A4@E/,f>CH>nr[:$OD_PuLYi#gC[Z4ah_61Q^\UT<uAtP3DS+0&B3`/rcbS4lOf`7sZsTqsTqL:)l88$0;ChTL_JnG@Q]6fi\@88B/N3DEr[;$TI\[Ea991Ta[pK1js?jif63Er$ZUUFJU(t)gY;uJJ4-08uTgla:k^0A"M)r7j(kfJPSo\\+IF`G:%^K!L(kB;%Q9ZHmp%:"YMCKGHhB#hWi;KnuUSW`1)]Sd-X\T=O)aoGTLD69"oofaV'1jG+'=M;1XONQ>!q,d8*!AIn0MkB=^jAatfmPRnBS[%=N3OOC#U^IoY*tDKJA*l>)rWX/.G;05*JGnI&ZLMnb-ZiNU6^r.!3R`Und8$<(SK\n:f)q(Kk6'F@34gjXsf#&t@lmY!<K')[t1^s!X=lu9QmCN.<4g`=TjG8p]*T2.9ZU*c;h)CA#p8oq=jg&oQ$P#<KS\/lPKW=W9q&N<\Eotq^mj_ic`ACDBoo<#`ual[/QqTtFXhpBI=(3=Xe;?N$`Z5gX8r9iOr4f*6hs*6oLg#orfp@eGM3,D"M?d=G9et4%$JUh,fch6,F%[]i^IF9:"^#\0)[U:?3[6>TT`WDe,1X9YJp)\7gdr52=4m(Aafe`<=/n2]bj`a`7GZE@)"KsCo5RA;4?7>Q>eVluTPaSE+oX3(*oIh-K\hV0?a)aHl8JlOmLThI6LEo:`k_()69E8TDP[ntWJ1eUe+.Ii=@0iKL2>UA2l$Ni3lR"'d,cNPfkr`bF\h#?AC`DmZK"@V>2j`TJ4*H>RpBCf7a%2LQ1tV]'7n,-dPOPJ#OB+q.CT(KI9$=GIY@E:Ni[&04k(+drbWI*ZM?^g!eI_;aSL4'[\Z[;4HA7^i(R)uk.'s>mgoO1Bh9]AYc+L+C[B:Sp#dAb&a@C*qg0r_k8,[4/eZ=,"VlaF0$s)SPH?<N0D:Tr#lD.Q-5Vo/?$e/klYUrq`"MX&2^<j6e9#(Hm=O?f2(2FsE(P>H8%q^dI0INH.:)7H=.taYE)t_<`$M'JN.ImPNV4\&9fL*Yt*A=c2liD!OA4SVKkI1=0]FA_4i1V;o`+26aP=!A6g+\\][on;%N4Jj+i0G>-L=3=h57(ks[4U3aFs(QHl30B7ee=_l/h#NcfDsi-KbhaRT(g,foiRn9lCc!L*d+]QF^B-['XT^gJph3j*=;QCY!W@HjKRTG+B0J]&a5b)[julp<bBlYK3,(:WGZV<_s!*adhP^BerJ6eIJDfh:e5E#P3G\@Fm,1pq]X,ln;pMr'WX+.W*k!11GTK_'KIP)kuF0e'PJ/D)loR+1(!'a7-F59>3O^4ahV7/f4L=*7N[sI#Yl_u2e?F@`WD'%F=iD49d6tnG>YL6,9Y0Pn>TZ^Nif$Njf`$4ge<l7+'ZuQPp[X/P<>+KI>:L,Yoe5n\XGh#f\D*i!#mgei>JPZ<Fs=q1tje`q>4P[jS>s7E==m1%\[\X-@BR&k@1P?Y%PtK$Q=3/50lB^kuI(46]r[J=Y#NOZ+1%m=eUDOY5fh]%gb3k)Sk-+1Y/[tZ7)&qU`#L(0BoX@3&rp`PHJ`P`l6VrR1G^u0T2AHQ.?<sJ'Z*eL,ZDa9AAZi?656Y>nq5@ih*%GB27`YeuhqT(T-Guh>.Fg=r/W,R1gBb?!$8ZDC-1hT0tO/i?J2q)?)%=SLeDpW56;mJ[AP17uN^tj^]egag0MHWg?;bTQAU^78oI3#;X=J6gCHh]tpBLeg^8c?fNO26aF$KhupT.oTOs@f$t4iDF".b,]P=8Y\SOkldrUU!8*m5;j.;0\Je,aap.-jXqldirKKn2'D)LlA4i!Y^[*6)'@Y*=Y@5'I@M]7%Acb1jP+uLbjf3r)[5JD=D\uZLOSN0"!07#k`8=qq!Du;D`pl9[I=K_h^??i/I?1MZJtZY\2h::Ti96>B[Mk@h(pdJ9B)efMd(O#K$TCQNH)Rs4C,=fojlALndrU%4D+ett=]:Rp+6?XI>O6g)XB%U88\)RW5'tCBSnXa:[>Fn`ofIefiIV\nPF"(XP&PKc#B"k<<R&a9<U$LYT>e5hh4;gr*gE=fjVme_1"h&1#XbWumt%ukg1&J8'R<<Y`FRlqJt5pf>RAaKHmqD>o?$0)1N!GGNYINr3FOn),(=_?,);S_G.%6H,)Y"="Zgl+/rGBsi$mkQ2W^VtMb(s"0r>/ADE0-2-GNZh*OJH"$cuiL/DdR-9.Y0nNZ@<D89k("lT+>-l?NGd]jH!md'TpEi)qEs7Ier\I$K$1JHo1<E@J:%i2sD914"ADqqKk\'mPm1m0:*ph%Y-KKg&EN1bGci8&Y0DJ+H0Xb-^TM'sD<@GKYTU]i?#,o?a`3aMSY2CqDtS*ZEl(;h^<0#EC9imDt:_A#]Y@T?[E@^)J5Hg*XjT#7Ofj_2.*,^l.21Qt?0Oe+2%C$9<i&mciEd!)lW#8U0_/rP3lEFa:!(h34n9gk)ur6"oT:EJMH@l931_dd2KrX$o)H5E(n)lM~>endstream
|
||||
endobj
|
||||
30 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2377
|
||||
>>
|
||||
stream
|
||||
Gatm=8U&oI&AII3bbabgGqfDOi7=A2-Z.S5MlS+i>\FJ--ui*U88e:i3s0GcCe;(YQP\*n0e5Er^4A9ur5[(.,R8QML"GcRDunEJE9To\IY;poakmcPAPpMo33MG"^'d^"H6SMK/LCW.LHKS[(I!)+kD6qF5mYLA!ZjV'@/b3+7#cT0]J(&p'\r;T?=#[Tc[=[+Fkk=O5rumC&^@QN+c<lDTH5U*=o"PZ-B8=TW$GbGkkeV:`uQL3?U,*m^_g01GaH6RP:"G6JrC\aM:%H!NeQC^M*@X[O5UIA*hiXQ1=]NII4+[.UM12S^]"/n"8lCV<1PS`15!oa;TVhS2^&74qJ^^[f4hA;%'J,UA`Y7?Kmj'O'hhttl%+:BMB#),n*0kNC5CeL<>9.1/!DR,>NL@QMmS9`)JK!]Qg:6TQs0ge);m:1k9"RVMLge+1PH-11aH58_'DX;b!F1knYGL1+9RJ2$U!\45^KS]O)Mlhm)Ka%_+3`/Z#1C&1_<QU,4;_m":\2[4L8'C%9P:*N6[)=^cAKcAKT-@S\PlP^4]tdK.:L-,!,m6j&,.3V=6OmqCDnL.[$;,IL!Gb$7A"[L&2+"2hsOdF)YIUZTLG3E4c(:g&@`#r%4?kjY""J)TqQPMR\>jfC)//_Bfp5IoL0Srj.C,+\"iFiNtV;Vu2P&C9;G^*7`B1d#h9`Su)H[=]W"(A[;L;bfHK<^%>\91DUs(^'lt@4qDL&CX^8Yh4)>nrV21b4k3RH+$[j(FoeLAVpF3Zp*_o=?!V&("\DrLW)Z]m3o-Y/4.)a1%V8gDa)cdo`<^`Y=N61MX%+H4WV.BnrG=PnYZ$?*N%Q[tkEA)ih(DN#s6'f>2+c*97@J`ZE<.t;'l*tWSRs74mYq6bX_RfjKKn#<_gtV2=H+K(?)\U>31oYZbaK@lT$:YIUmPuY`Or;1[Og$U>cluPc0+C+R<HW$2\Li@&Xe*JXbT9XDHKK3;ft:Kj4H0hRP`p7]=S"6%dm6.6o=5s"q+#)X)4T[Z8d:gKJ3V)On)N;&W%P4>&VSnSh=?c9PomSW$4g`,>PsM9\VO$^>Eh/)W=q<1M)-NWQGs=XTpua"okbI6/O8bap5QkLW%c+fa$o,B4gd'6Q'SBO'$HoR2Pt-Rph`;:d+,/"abt+YP[8FM%#X9]6`@5[c_;t]lT/"H.dRpC<AN=+#YZ<P=#pZ^V`o>,*sWsm>2BB1FBE[1@67T-CW4"?\VEFJU'%b'hD+[DLh_/7Ztl#K@OE?gSZEQMtN;1-Xl/,iLuH9^3sc=Gq3F]AZak61`"YCB&r[4Oe:ht)JA7CUFr_@k?(.NqpqZOep>dupVD*hqrrJkN3Q5i6QJS)[jl`?10>,=Fm(*]^;&b3^[6VmmkaC"L@=(-!Uh?t\H3+:'X0Cs[e`q+'kc1ZVUto"\1=WQ4=fl\<i4^]luSfe"ikHP22ZiQ=$5Nc0++#ARVO<LKg0$CfbulJN]21m6W3&lWjO;rpeY?1`PN@nFh1c:dCd/F("0EJ?@u.s6dsQoa"ufVn8F/k[R(.75Sot8gP4b,>O'<HC5rO-X^BJFJ+;l$N>9Ti"4o8fCG_IAZ#`)dEj1.jUuj-Z@#7HP.<`d*F\=nrUl25unlN1Qc!Zj_kN$O^].-:ML_H8@?fq,\R0c]YPh)<TeTFZj/hl?QTpf&;@2_71bpV-L#48t2!Piis+R#Tp'XkkN"HQqGYe.ooaK\,G8>WQZL#[rkkf:EjC<_9UF<*1THB-2IYZJd-)QGY\K*6SnbZ_tB@(dgA3iQTARb5i!*mAkWk+70oAVJ$7G>AhE%U[Z*G48cnHEMlj=E1f]"/3oo^taY14n#Y,(/QW:%A#u:HEV&GjQW\;%CSO[,g3]"+=BERjg:.bQ,eU8"2T!<PB0'LjP1POc*2&_%G8K.JPNT:3"3n2.]]=hE(M!4L)c\sN"Vl"Dbb:0$'?4Po&gOJmS-hl;**8%8-mm^7]0PTNkI$g<NN[[M.T#)58FkbTt/0nD\n1Q$K#0[Ekr\&D\t[GH)GJh@j)4T=V5/;kU1LWD::?DWRrAQY`jsNAePj@cXVBL.usk8:K0ks?2^E:9rD%$X*8-`-ulTNpZBe38#n/e)X$'BEn"E^MkH\]rYRu1\OYLCT,Ff]'1R<W[:=\W(o83-=&``ATiR/`.hB2qKT70Bg`)gHU:_58WW2G^l)p#FftcVqZIe1Kh)XNR)2CuiM?bSgV()*Befs$3l%+a3<GsZ6a3AJLORHEkJC*1_MAA+S6ulI3h[OoU'!tTf1.FOtOQ<KBgVD6QXYT:Z4/.^8[UV5)o_4n+>p6iXH8Pk:KLk9&+sqImUDj1*lK[O+:E@tQ-;'`iWig@=.*VA.rW,Pi2;G0GNC<CEC16ZT6B54L~>endstream
|
||||
endobj
|
||||
31 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2265
|
||||
>>
|
||||
stream
|
||||
Gb"/)>>s99'RnB33%phr/I5W5=mjMZ/;:N*U#!.f]<a<+30'.=*':PK./igc,[X(J$%Xi6bg6a+/=l[cVfQYk;#JMlcY*aV[L:jY)))QV!AKRZcom&+h9M&k&N%ka"!XPh8c[7*o"]m*fL_3-Z[d#7(!&WQmRm_R'BT,8LYT]'<JL]7]hbKU*3Yl('jT16,Z%G@@tFt<20adI(>'?:]gd#S!:m,QK8TZ_LCTGcN_FgmZr5*MaU$upE>X>hBEJ]USgA@*WNF\$aFL5C7cL+$We#C9lA1TK`<XV%L]jgQ<%CBcU8?D=4,/a@It-@2^TsO^U/@4&n5#IABN2i'k<A8!Ds\6->a2)rHfiNtfjnpIng/BRT_llhT&D=(O2qM(LSfS%%SW>276c^-QG-i-,ojp=#`R'5qhjoYoi8E6^>K,7l=6>cLN>p&$He7`Ia<R*DfM*T)b2&$E#+aNS.=G,7)^dO)kNMf4M3*)'Y.Nb:9AB)<Fp&6j'/@&]AQ3B7F.Wd:kl[%?oCD!6qKL\BgXVpSeOS=6Jrus*+18ANWLDl_PE>q"@Y7j^bAiRGTWWQ_BoL7Xi;=X7p@0PR70kS![IL7'r7B:-LEI"FMf0)1)kqUE)7i=Fc7ic]=g_jhW+"KCV5:Kd?R(d*A)reCI;+h?C07_hkl65%*@,%TkQYCf(-JR4PtCfnpKh.3ofqVS*ek>YoQA.7@T3U5APlI6CjW[V9`@QU_U(+&*)UA*bMe9+3[fXcf"*]Wm_RRAEA$YfA7`bkretp$r:;J9\q_,#Do='Olb?.N<f)L;2/fBUq!(Ns)pRH_B@1QEf>@fj92/eg4s#>::%UgKOSDbGS$@n9e^8pbDi2M",O6>b"ZU./BdUU0a[CrfBg;6q#BDE&A?obg>h3D+[*[K7dp]Ac"2h7eK-%_Sd<gi]<Ho7](9(ciAj6pe=\FfI!]'r+b]_26VoRu3nF\<f%/jEXJi)C2+MhTRC*'PHnHAleu"nTnf1,R95mW4MSr+>`Anh"G/7+o\`PH0<C.6<fVA"'EUHI4VXQ&#S+X$]H>@g+)=)1[]!ot/4,;YjO*_Ab$pr\O6UK^E'G&@<h6=A&;(ijLn]:dV_\,]QSAtrQ>Ah_qEt0E?ZQu"o#n%%+bm`>!0td4*0:SOt6enGf3%4o>``GDFp"KXG$0%oC^*dce,!FNc\GUSBK\OKB_#,b8a#?esjn`V;]+Nr/.-?/#';9a_)]rnTeMe$LD&7pc-WAdTfilT(aPl$K?sBut9+(fn@tU-P!u$u![,MTeWs@$#/.(ZTj'J6bRFhY.3["sC/*tXX`12PV`&o:%,=+M/N"B/E76Ze.m=M6hI&gW(4:(THRGN%1bN_j$[G:e@IQ(>Ph^nt"fm&[IT*?t``>a_D,n.-/;hK5H3"0jV/eA/jJW6qU_[cBLP*I7j@'f0.[f,'pb4_2q<R37QKHqLG^kR%V[cLt9-@+177bE$bG>fVX_rRQ%48g<$q-9g])RL8X'918n;R@3E[0rQ=>'!oFii_DNs4@>)r-6jqNk:+rnBEh5lbbUSfCJqt>i,tAHfgYd"rII:NY.tq?PS\bqcf0l9pjF1jTqd?-O]:V&a&F*JZ^HE!ie^1T5M(XG_&PtWa:)e$:K1k7jE@3N8q3g#.d;+FBiN2H`4[ZFR9./8:Y#P^&UHt=g3?=?:'3^WT^rn4>7SbbU28q.*KeI>-@W]e0'>23Uu)fOI1<.I]*QcN)q0af[_jj@57-!ZudbUY)/-uf%LSs,pd[rn)5K&RtD',I0C@,BYs9/2/DO`;I:[3^ge48G&K3:oOcDJ>o9c=nb!78*Z?R!&)AhiWe3dJfi>9g0"Z9NCl"E)'Ghg1+/&)@]1LX*ls!#Q;kC#DD&.,JgbU-u7pu_W`2&a'4]`KNaBW7\Z.j^\+L'Xf.to1j4F)?7;o;dj%G5PjhO;F'm^QUW]1sdaqNbf7]`Zu4peYGmT<&XYgWHWqW]$meFo@&@Kn8f$,.usm(gYn:/sn!S,JK.7Zb\f]5WlICSrR0\0Nd+'7!FO;29g"!rN<\MGE_JA`;=6]VZu#Nj2Esb$V'%.(-Yi=$Qn078o'H`R1n0^X3.J(HY00(D5[:l,6*g<8K$\$PF78.$HEY$H-OdF"'-)lef9E.`2_''n<sTj)Il>!@231n!m+_6kJm5jCMJOUP+h/'Pc'01:hTHo@:78?@EZ8`86%lTPma^p'RTKZUU]4KD]Wo-g't6c(tE2.>OdYR<'3$2qh3\\@VWHVenV@ZWk8\0rrL2REhN~>endstream
|
||||
endobj
|
||||
32 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1392
|
||||
>>
|
||||
stream
|
||||
Gatm:gN)%,&:N/3lq:qqG7<M,n>YFnFm:m6,aFh4oS'#JM&%`o8_Z'"qCY&&,0@.U9ZCS[$f[[>$S3(:JDq+2J("JWm/eoD)))PY4(+4B=F>do(UJU'7*sR_#t-1YZiFQgij"/_2(K8T=A1^7)H<dDO-BjoM2ibq6#GTKHq*+GO/Oggj2dY:nVEMWel2`]JdA+[8AZ+"h22\\&2%EWSdlidpcgsdLk7!"M't)^1C8pNb(\tSJDqCJN@/d-b\&(kj+<5H%P\TQm\g.1-#,hDMW+b[$5((j:(:N_-:U!aMZV1Qs7GsgI]6AV8_6t-_k1d_drc%JEur0Jc,E]3A5-iXiOsZHH*o5?g&:u0HWZe"f+WTF9;2pk5M?<Nps#+9f%a<jK>VooT9E8U-Fdn+\hA;7#C<f+"?/0SF2lOEQ54Je-Hk3(%]:nSH+([tP_^eSb*XS+"<'9kgq60LKn`R$S&ZCVb\0X?7jHS+eQW56"#F\b@?cc^S:OL$juoh&kQ%E=0Ql5l"Wc09"8.OM=MnM/:sRqMo(F3+'=mH%oP-%h)DY@Ba<2suN(&'6\:BIdF!/+W_"oAB>Bote]_r;9reC"&IeMrAn1&QN4GC#uSdUl\Qe/l_&<>`9.W$uRP<mY;i^+9-Q+r$!^96`ZI;&B-WJ;Y9<'$<ScVJLeAAhl*4Q;LjH@/d/gT%OOl$.N(LSPs@jY`p=@m2LNh6hD)j,J7@8i_Ms#ZH<6ls4TMQ9D.fJ7.7ZpoB1Y8oXrRgKI.re8AJpg[auC][jd_gkY*FpI2B8+i<\g\o6p"#nPWfdKPrjk&b.;FSfl^f8MrSkZ@\qruY%#Y85*%YL.CZ:l17/mPOe/K@L^SH!`rd82YkS4-]YnP"lmIa4CZ2\4QH@@O3opFq6&iN0Fl`oiBiJmn@(SqbfV[m-Yaa6q\2K.a'lJa%OC.LerHe+e?lurM&+@JjVpC`QRP<B("5rCJS1?%?k3K![Pja]jU+I/FOq[Yj'OL7piPE_bEtZ)Y?X^KJ?.2C'J2[1`kPG^%7g*.eeQ'/7"3ud!!]Ck7f7Ud%U.j3p"!J.+3MK+TsM4Qf<-KZ@EN!(>rht6S?uNomW9@V<;e,#9227=+FSRNW_RC0XQ:T>jt(DUP$m+GDD-SCtdrX#AXGaD*:#r.As.r<.\g?d^Zd$k?%(6FE_p2VfKY$OB_#!Z-2jF,Ri2'4ji3#gf!M2+_YsL`UEr$C+5O^g97QNGYW#?#2K7"XF)=BT=$A[YY4k\TP7hEl`t6*4*XFknD,^J%sD)?A/eY*(6Z=]126;iojm@t/@0[f>pPp0D*)f@2nOO[)SaL^IUua:9iX#e[^*->X*`V\>8$^7-S&F#"S:4,n?7Pd/Fi5b2A_jY9TknceGM!!#A3u:.2M)(~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 33
|
||||
0000000000 65535 f
|
||||
0000000061 00000 n
|
||||
0000000143 00000 n
|
||||
0000000250 00000 n
|
||||
0000000362 00000 n
|
||||
0000000467 00000 n
|
||||
0000000672 00000 n
|
||||
0000000749 00000 n
|
||||
0000000954 00000 n
|
||||
0000001064 00000 n
|
||||
0000001269 00000 n
|
||||
0000001475 00000 n
|
||||
0000001591 00000 n
|
||||
0000001797 00000 n
|
||||
0000002003 00000 n
|
||||
0000002209 00000 n
|
||||
0000002415 00000 n
|
||||
0000002621 00000 n
|
||||
0000002827 00000 n
|
||||
0000003033 00000 n
|
||||
0000003103 00000 n
|
||||
0000003384 00000 n
|
||||
0000003516 00000 n
|
||||
0000005454 00000 n
|
||||
0000008640 00000 n
|
||||
0000011552 00000 n
|
||||
0000014506 00000 n
|
||||
0000016699 00000 n
|
||||
0000019673 00000 n
|
||||
0000021957 00000 n
|
||||
0000024573 00000 n
|
||||
0000027042 00000 n
|
||||
0000029399 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<455dfa93e2c45e38d8015a37292134e7><455dfa93e2c45e38d8015a37292134e7>]
|
||||
% ReportLab generated PDF document -- digest (opensource)
|
||||
|
||||
/Info 20 0 R
|
||||
/Root 19 0 R
|
||||
/Size 33
|
||||
>>
|
||||
startxref
|
||||
30883
|
||||
%%EOF
|
||||
@@ -1,943 +0,0 @@
|
||||
# Les 9 -- Slide-overzicht
|
||||
## Cursor Deep Dive: Van Agent Harness tot Code Review (18 slides)
|
||||
|
||||
**Cursus:** AI Developer -- NOVI Hogeschool Utrecht
|
||||
**Duur:** 3 uur (180 minuten) | 09:00 - 12:00
|
||||
**Project:** LinkVault -- een bookmark manager (in-memory, geen database)
|
||||
|
||||
> **Voorkennis studenten:** Cursor basics uit Les 3 (Chat, Inline Edit, Composer, Tab, @ Mentions, .cursorrules, Skills/Docs). Next.js 16, TypeScript, Tailwind uit eerdere lessen. Studenten hebben Cursor al geinstalleerd met een actief abonnement.
|
||||
|
||||
---
|
||||
|
||||
## Timing-overzicht
|
||||
|
||||
| Tijd | Duur | Onderwerp | Slide(s) | Vorm |
|
||||
|------|------|-----------|----------|------|
|
||||
| 09:00 - 09:10 | 10 min | Welkom + terugblik | 1 - 2 | Klassikaal |
|
||||
| 09:10 - 09:35 | 25 min | Theorie: Agent Harness, Rules, Plan Mode, Debug Mode, Code Review | 3 - 9 | Klassikaal |
|
||||
| 09:35 - 09:55 | 20 min | Setup: nieuw project + 3 .cursor/rules bestanden | 10 - 11 | Samen |
|
||||
| 09:55 - 10:15 | 20 min | Plan Mode: plan LinkVault, review, Build | 12 | Klassikaal |
|
||||
| 10:15 - 10:30 | 15 min | Pauze | -- | -- |
|
||||
| 10:30 - 11:05 | 35 min | Agent Mode: features bouwen | 13 | Klassikaal |
|
||||
| 11:05 - 11:25 | 20 min | Debug Mode: Tim's bug vinden en fixen | 14 | Klassikaal |
|
||||
| 11:25 - 11:45 | 20 min | Code Review & Testing + Semantic commits + Git push | 15 - 16 | Klassikaal |
|
||||
| 11:45 - 11:55 | 10 min | (Optioneel) PR review met Cursor | 16 | Klassikaal |
|
||||
| 11:55 - 12:00 | 5 min | Afsluiting + huiswerk | 17 - 18 | Klassikaal |
|
||||
|
||||
---
|
||||
|
||||
## Slide-indeling
|
||||
|
||||
---
|
||||
|
||||
### Slide 1: Titelslide
|
||||
**Timing:** 09:00 - 09:05 (5 min)
|
||||
|
||||
**Titel:** Les 9 -- Cursor Deep Dive
|
||||
**Ondertitel:** Van Agent Harness tot Code Review -- LinkVault bouwen
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| |
|
||||
| LES 9: CURSOR DEEP DIVE |
|
||||
| |
|
||||
| Van Agent Harness tot Code Review |
|
||||
| LinkVault bouwen met 5 Agent-technieken |
|
||||
| |
|
||||
| Tim -- NOVI Hogeschool Utrecht |
|
||||
| |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Welkom bij les 9. Vandaag gaan we diep in Cursor.
|
||||
- We bouwen een complete app: LinkVault, een bookmark manager.
|
||||
- Jullie hebben Cursor al draaien met een abonnement -- we gaan er direct mee aan de slag.
|
||||
|
||||
---
|
||||
|
||||
### Slide 2: Planning Vandaag
|
||||
**Timing:** 09:05 - 09:10 (5 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| PLANNING VANDAAG |
|
||||
| |
|
||||
| 09:00 Welkom + terugblik |
|
||||
| 09:10 Theorie: hoe werkt een coding agent? |
|
||||
| 09:35 Setup: project + rules |
|
||||
| 09:55 Plan Mode: LinkVault plannen |
|
||||
| 10:15 -- PAUZE -- |
|
||||
| 10:30 Agent Mode: features bouwen |
|
||||
| 11:05 Debug Mode: bug opsporen |
|
||||
| 11:25 Code Review + commits + push |
|
||||
| 11:45 (Optioneel) PR review |
|
||||
| 11:55 Afsluiting + huiswerk |
|
||||
+========================================================+
|
||||
|
||||
LEERDOELEN:
|
||||
+----------------------------------------------------+
|
||||
| 1. Agent Harness begrijpen (rules + tools + model) |
|
||||
| 2. .cursor/rules met YAML frontmatter schrijven |
|
||||
| 3. Plan Mode workflow beheersen |
|
||||
| 4. Debug Mode systematisch toepassen |
|
||||
| 5. Code Review + semantic commits + push |
|
||||
+----------------------------------------------------+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Dit is het programma. We bouwen alles stap voor stap in Cursor.
|
||||
- Aan het eind hebben jullie een werkende LinkVault-app met nette commits op GitHub.
|
||||
- De PR review is optioneel -- alleen als we tijd over hebben.
|
||||
|
||||
---
|
||||
|
||||
### Slide 3: Hoe werkt een Coding Agent?
|
||||
**Timing:** 09:10 - 09:15 (5 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| HOE WERKT EEN CODING AGENT? |
|
||||
| |
|
||||
| Een coding agent is een LLM met een "harness": |
|
||||
| een systeem dat het model aanstuurt. |
|
||||
| |
|
||||
| +--------------------------------------------------+ |
|
||||
| | AGENT HARNESS | |
|
||||
| | | |
|
||||
| | +------------+ +----------+ +--------+ | |
|
||||
| | |INSTRUCTIONS| | TOOLS | | MODEL | | |
|
||||
| | | | | | | | | |
|
||||
| | | .cursor/ | | file edit| | Claude | | |
|
||||
| | | rules/ | | search | | GPT-4o | | |
|
||||
| | | prompts | | terminal | | etc. | | |
|
||||
| | +-----+------+ +----+-----+ +---+----+ | |
|
||||
| | | | | | |
|
||||
| | +-------+-------+------+-------+ | |
|
||||
| | | | | |
|
||||
| | [ AGENT LOOP ] | |
|
||||
| | think -> act -> observe -> repeat | |
|
||||
| +--------------------------------------------------+ |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- **Instructions:** regels die je meegeeft (system prompt + rules bestanden)
|
||||
- **Tools:** wat de agent KAN doen (bestanden lezen/schrijven, terminal, zoeken)
|
||||
- **Model:** het LLM dat beslissingen neemt (Claude, GPT-4o, etc.)
|
||||
- De agent draait in een **loop**: denken, actie, observeren, herhalen
|
||||
- Cursor IS zo'n harness. Jij geeft de instructions, Cursor levert tools + model.
|
||||
|
||||
**Spreektekst:**
|
||||
- Een coding agent is meer dan een chatbot. Het is een LLM dat in een harness draait — een systeem met drie onderdelen.
|
||||
- Instructions: dat zijn de regels die je meegeeft. Jouw rules-bestanden plus de system prompt van Cursor zelf.
|
||||
- Tools: de agent kan bestanden lezen, bewerken, door je code zoeken, en commando's uitvoeren in de terminal. Dat is het verschil met ChatGPT — hij kan daadwerkelijk dingen doen in je project.
|
||||
- Model: het AI-model dat de beslissingen neemt. Claude, GPT-4o, et cetera.
|
||||
- De agent werkt in een loop: denken, actie uitvoeren, het resultaat bekijken, en herhalen. Net zoals jij als developer zou doen.
|
||||
- Cursor IS zo'n harness. Jij levert de instructions via rules-bestanden, Cursor levert de tools en het model.
|
||||
|
||||
---
|
||||
|
||||
### Slide 4: Rules & Skills
|
||||
**Timing:** 09:15 - 09:20 (5 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| RULES & SKILLS |
|
||||
| |
|
||||
| .cursor/rules/ directory -- drie types: |
|
||||
| |
|
||||
| +--------------------------------------------------+ |
|
||||
| | TYPE | HOE? | VOORBEELD | |
|
||||
| |-----------------|----------------|----------------| |
|
||||
| | Always-on | alwaysApply: | project.mdc | |
|
||||
| | | true | (tech stack, | |
|
||||
| | | | conventies) | |
|
||||
| |-----------------|----------------|----------------| |
|
||||
| | Auto-attached | globs: | components.mdc | |
|
||||
| | | "src/**/*.tsx" | (component | |
|
||||
| | | | regels) | |
|
||||
| |-----------------|----------------|----------------| |
|
||||
| | Agent-requested | description: | api-design.mdc | |
|
||||
| | | "use when..." | (wordt opge- | |
|
||||
| | | | vraagd) | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| VUISTREGEL: Houd rules MINIMAAL en SPECIFIEK. |
|
||||
| Te veel regels = ruis = slechtere output. |
|
||||
| |
|
||||
| SKILLS = dynamische rules (bv. @docs, @web). |
|
||||
| Cursor kan zelf beslissen welke skills te gebruiken. |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- Elke `.mdc` file heeft YAML frontmatter bovenaan
|
||||
- **alwaysApply: true** = altijd meegestuurd naar het model
|
||||
- **globs** = alleen meegestuurd als je werkt in bestanden die matchen
|
||||
- **description** = agent kan het zelf ophalen als het relevant lijkt
|
||||
- Minder is meer. 3-5 regels per bestand. Geen roman schrijven.
|
||||
|
||||
**Spreektekst:**
|
||||
- In Les 3 hadden jullie een enkel .cursorrules bestand. Dat werkt nog steeds, maar Cursor heeft nu een veel krachtiger systeem: de .cursor/rules directory.
|
||||
- Elk bestand is een .mdc file — Markdown Configuration — met YAML frontmatter bovenaan.
|
||||
- Er zijn drie types. Always-on rules met alwaysApply: true — die staan altijd aan. Gebruik dit voor je project.mdc met tech stack en conventies.
|
||||
- Auto-attached rules met een globs patroon. Werk je in een TSX-bestand? Dan laadt Cursor automatisch je components.mdc. Slim en efficiënt.
|
||||
- En agent-requested rules: de agent kan zelf rules opvragen als hij denkt dat ze relevant zijn. Dat gebruiken we vandaag niet actief.
|
||||
- Vuistregel: houd je rules kort en specifiek. Teveel regels is ruis, en ruis levert slechtere output.
|
||||
- Skills zijn dynamische rules die Cursor automatisch kan laden. Die laten we even voor wat het is vandaag.
|
||||
|
||||
---
|
||||
|
||||
### Slide 5: Context Management
|
||||
**Timing:** 09:20 - 09:25 (5 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| CONTEXT MANAGEMENT |
|
||||
| |
|
||||
| Het model heeft BEPERKT werkgeheugen (context window). |
|
||||
| Elk gesprek begint "leeg". Nieuwe feature = NIEUWE CHAT.|
|
||||
| |
|
||||
| CONTEXT TOEVOEGEN: |
|
||||
| +--------------------------------------------------+ |
|
||||
| | @file | specifiek bestand meegeven | |
|
||||
| | @folder | hele map als context | |
|
||||
| | @codebase | Cursor doorzoekt je project | |
|
||||
| | @web | zoekt het internet | |
|
||||
| | @docs | doorzoekt documentatie | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| TIPS: |
|
||||
| +--------------------------------------------------+ |
|
||||
| | - Combineer: @file layout.tsx + @docs Next.js | |
|
||||
| | - Nieuwe feature? Start ALTIJD een nieuwe chat | |
|
||||
| | - Lang gesprek? Context raakt vervuild | |
|
||||
| | - Geef RELEVANTE context, niet ALLES | |
|
||||
| +--------------------------------------------------+ |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- Context window = werkgeheugen van het model. Alles wat je toevoegt kost ruimte.
|
||||
- Nieuwe feature = nieuwe chat. Dit is de belangrijkste tip van vandaag.
|
||||
- @codebase laat Cursor zoeken, maar wees specifiek in je vraag.
|
||||
- Combineer @ mentions voor maximale relevantie.
|
||||
|
||||
**Spreektekst:**
|
||||
- Context is misschien het allerbelangrijkste concept. De agent weet alleen wat je hem vertelt.
|
||||
- Denk aan de agent als een briljante developer die net op je project begint. Hij kent de taal en frameworks, maar JOUW code kent hij niet. Jij moet hem vertellen waar hij moet kijken.
|
||||
- Met @ mentions voeg je context toe: @file voor een specifiek bestand, @folder voor een hele map, @codebase om Cursor je project te laten doorzoeken.
|
||||
- Je kunt ze combineren: '@data.ts @BookmarkCard.tsx — voeg een edit knop toe'. Dan heeft de agent precies de bestanden die hij nodig heeft.
|
||||
- De allerbelangrijkste tip: nieuwe feature, nieuwe chat. Na teveel heen-en-weer raakt de context vervuild en gaat de agent afdwalen.
|
||||
|
||||
---
|
||||
|
||||
### Slide 6: Plan Mode
|
||||
**Timing:** 09:25 - 09:30 (5 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| PLAN MODE (Shift+Tab) |
|
||||
| |
|
||||
| Start ELKE feature met een plan! |
|
||||
| |
|
||||
| WORKFLOW: |
|
||||
| +--------------------------------------------------+ |
|
||||
| | | |
|
||||
| | 1. RESEARCH Vraag Cursor om te onderzoeken | |
|
||||
| | | (bestaande code, docs, structuur) | |
|
||||
| | v | |
|
||||
| | 2. VRAGEN Stel vragen, krijg opties | |
|
||||
| | | | |
|
||||
| | v | |
|
||||
| | 3. PLAN Cursor schrijft plan in markdown | |
|
||||
| | | | |
|
||||
| | v | |
|
||||
| | 4. EDIT JIJ reviewt en past het plan aan | |
|
||||
| | | | |
|
||||
| | v | |
|
||||
| | 5. BUILD Tab terug naar Agent Mode | |
|
||||
| | en voer het plan uit | |
|
||||
| | | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| WAAROM? Voorkomt dat de agent meteen gaat bouwen |
|
||||
| zonder na te denken. Jij houdt de controle. |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- Shift+Tab schakelt naar Plan Mode
|
||||
- De agent denkt NA maar voert NIET uit -- geen file edits
|
||||
- Jij reviewt het plan voordat er code geschreven wordt
|
||||
- Na goedkeuring: Tab terug naar Agent Mode om het plan uit te voeren
|
||||
- Dit is hoe professionals met agents werken
|
||||
|
||||
**Spreektekst:**
|
||||
- Dit is DE belangrijkste workflow van vandaag. Plan Mode. Begin ALTIJD met een plan.
|
||||
- Hoe werkt het? Open de chat met Ctrl+L, druk Shift+Tab om naar Plan Mode te schakelen. De agent gaat dan NIET meteen code schrijven — hij doet eerst research en maakt een stap-voor-stap plan.
|
||||
- Dat plan is bewerkbaar. Jij kunt stappen schrappen, toevoegen, herschrijven. Agent te ambitieus? Schrap die stappen.
|
||||
- Pas als je tevreden bent, schakel je terug naar Agent Mode en laat je het plan uitvoeren.
|
||||
- Waarom zo belangrijk? Geen plan betekent dat de agent z'n eigen gang gaat. Dan ben je uren bezig met fixen. Een goed plan levert gerichte output en minder iteraties. Dat spaart je requests.
|
||||
|
||||
---
|
||||
|
||||
### Slide 7: Agent Mode vs Ask Mode
|
||||
**Timing:** 09:30 - 09:33 (3 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| AGENT MODE vs ASK MODE |
|
||||
| |
|
||||
| +------------------------+ +----------------------+ |
|
||||
| | AGENT MODE | | ASK MODE | |
|
||||
| | (Tab) | | (Tab Tab) | |
|
||||
| | | | | |
|
||||
| | - VOERT UIT | | - ANTWOORDT ALLEEN | |
|
||||
| | - Edit bestanden | | - Geen file edits | |
|
||||
| | - Draait terminal | | - Geen terminal | |
|
||||
| | - Maakt nieuwe files | | - Alleen uitleg | |
|
||||
| | | | | |
|
||||
| | Gebruik voor: | | Gebruik voor: | |
|
||||
| | - Features bouwen | | - Code begrijpen | |
|
||||
| | - Refactoring | | - Concepten leren | |
|
||||
| | - Bug fixen | | - Architectuur- | |
|
||||
| | - Tests schrijven | | beslissingen | |
|
||||
| +------------------------+ +----------------------+ |
|
||||
| |
|
||||
| VUISTREGEL: |
|
||||
| "Moet er code veranderen?" -> Agent Mode |
|
||||
| "Wil ik iets begrijpen?" -> Ask Mode |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- Agent Mode is de standaard -- hier bouw je mee
|
||||
- Ask Mode is voor vragen stellen zonder dat er iets verandert
|
||||
- Wisselen met Tab (Agent) of dubbel Tab (Ask)
|
||||
- Plan Mode (Shift+Tab) is een derde optie: denken zonder uitvoeren
|
||||
|
||||
**Spreektekst:**
|
||||
- In Cursor heb je twee modes in de chat: Agent en Ask. Het verschil is simpel.
|
||||
- Ask Mode is voor vragen. 'Hoe werkt useEffect?' of 'Leg deze code uit.' De agent leest, maar raakt niks aan.
|
||||
- Agent Mode is voor actie. De agent kan bestanden aanmaken, bewerken, verwijderen, en terminal commando's uitvoeren. Multi-file edits, refactoring, nieuwe features — dat doe je in Agent Mode.
|
||||
- Vuistregel: moet er code veranderen? Agent Mode. Wil je iets begrijpen? Ask Mode.
|
||||
- Vandaag werken we vooral in Agent Mode en Plan Mode.
|
||||
|
||||
---
|
||||
|
||||
### Slide 8: Debug Mode
|
||||
**Timing:** 09:33 - 09:36 (3 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| DEBUG MODE |
|
||||
| |
|
||||
| NIET: error kopieer-plakken en hopen op het beste. |
|
||||
| WEL: systematisch debuggen met de agent. |
|
||||
| |
|
||||
| WORKFLOW: |
|
||||
| +--------------------------------------------------+ |
|
||||
| | | |
|
||||
| | 1. HYPOTHESES "Wat kan dit veroorzaken?" | |
|
||||
| | | | |
|
||||
| | v | |
|
||||
| | 2. LOGGING Voeg console.log / breakpoints | |
|
||||
| | | toe op strategische plekken | |
|
||||
| | v | |
|
||||
| | 3. REPRODUCE Reproduceer de bug | |
|
||||
| | | | |
|
||||
| | v | |
|
||||
| | 4. ANALYZE Bekijk output, beperk scope | |
|
||||
| | | | |
|
||||
| | v | |
|
||||
| | 5. TARGETED FIX Pas alleen aan wat nodig is | |
|
||||
| | | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| TIP: Geef Cursor de CONTEXT. Welk bestand? |
|
||||
| Welke functie? Wat verwacht je? Wat gebeurt er? |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- Debug Mode is een mindset, niet een knop
|
||||
- Stuur de agent met hypotheses, niet met "fix this"
|
||||
- Voeg logging toe VOORDAT je gaat fixen
|
||||
- Gerichte fix > shotgun approach
|
||||
|
||||
**Spreektekst:**
|
||||
- Debug Mode is een aanpak, niet een knop. Het verschil: bij een simpele bug plak je de error in de chat en de agent fixt het. Dat werkt voor de makkelijke gevallen.
|
||||
- Maar bij complexe bugs heb je een systematische aanpak nodig. De agent genereert hypotheses over wat er mis kan zijn, voegt logging toe op strategische plekken, vraagt jou om de bug te reproduceren, analyseert de logs, en stelt dan een gerichte fix voor.
|
||||
- Niet gokken, maar bewijzen. Dat is het verschil.
|
||||
- We gaan dit straks live doen. Ik ga bewust een bug in onze code plaatsen en dan laten zien hoe je die systematisch opspoort.
|
||||
|
||||
---
|
||||
|
||||
### Slide 9: Code Review & Testing
|
||||
**Timing:** 09:36 - 09:40 (4 min -- afronden theorie)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| CODE REVIEW & TESTING |
|
||||
| |
|
||||
| 1. FIND ISSUES (Cursor's built-in feature) |
|
||||
| Cursor scant je code op problemen |
|
||||
| |
|
||||
| 2. TESTS SCHRIJVEN |
|
||||
| Vraag Agent Mode om tests te genereren |
|
||||
| Prompt: "Schrijf unit tests voor [bestand]" |
|
||||
| |
|
||||
| 3. SEMANTIC COMMITS |
|
||||
| +--------------------------------------------------+ |
|
||||
| | WORKFLOW: Bouw ALLES eerst. Aan het eind: | |
|
||||
| | | |
|
||||
| | feat: nieuwe feature | |
|
||||
| | fix: bugfix | |
|
||||
| | refactor: code verbetering | |
|
||||
| | test: tests toevoegen | |
|
||||
| | docs: documentatie | |
|
||||
| | chore: overig (config, deps) | |
|
||||
| | | |
|
||||
| | -> Bouw alles in 1 sessie | |
|
||||
| | -> Aan het einde: splits in logische commits | |
|
||||
| | -> Dan pas push naar GitHub | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| 4. PR REVIEW (optioneel, als tijd het toelaat) |
|
||||
| Cursor kan een PR reviewen als code reviewer |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Kernpunten:**
|
||||
- Find Issues = Cursor's self-review feature
|
||||
- Tests schrijven met Agent Mode (niet handmatig)
|
||||
- Semantic commits: NIET na elke wijziging committen
|
||||
- Realistisch: bouw alles, splits achteraf in nette commits
|
||||
- PR review is optioneel vandaag
|
||||
|
||||
**Spreektekst:**
|
||||
- Het laatste theorieblok, en misschien het belangrijkste voor je professionele ontwikkeling.
|
||||
- Voordat je iemand anders vraagt om je code te reviewen, doe je altijd eerst een self-review. Dat geldt ook voor AI-geschreven code. Cursor heeft een Find Issues feature die je code scant op problemen.
|
||||
- Daarnaast kun je de agent vragen om tests te schrijven. Tests zijn je vangnet.
|
||||
- En dan semantic commits. In plaats van een grote commit met 'app gebouwd', verdeel je je wijzigingen in logische groepen met prefixes als feat:, fix:, test:.
|
||||
- Maar — en dit is belangrijk — je hoeft niet na elke stap te committen. Vandaag bouwen we alles eerst af, en aan het eind splitsen we de diff op in nette commits. Dat is een realistische workflow.
|
||||
- Dezelfde standaard voor AI-code als voor handgeschreven code.
|
||||
|
||||
---
|
||||
|
||||
> **CHECKPOINT THEORIE**
|
||||
> Vraag aan de klas: "Noem de drie onderdelen van een Agent Harness."
|
||||
> Verwacht antwoord: Instructions, Tools, Model.
|
||||
|
||||
---
|
||||
|
||||
### Slide 10: Setup -- Nieuw Project
|
||||
**Timing:** 09:40 - 09:50 (10 min -- samen doen)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| SETUP: NIEUW PROJECT |
|
||||
| |
|
||||
| STAP 1: Maak een nieuw Next.js 16 project |
|
||||
| |
|
||||
| $ npx create-next-app@latest linkvault |
|
||||
| |
|
||||
| TypeScript? Yes |
|
||||
| ESLint? Yes |
|
||||
| Tailwind CSS? Yes |
|
||||
| src/ directory? Yes |
|
||||
| App Router? Yes |
|
||||
| Import alias? @/* (default) |
|
||||
| |
|
||||
| STAP 2: Open in Cursor |
|
||||
| |
|
||||
| $ cd linkvault |
|
||||
| $ cursor . |
|
||||
| |
|
||||
| STAP 3: Maak de rules directory |
|
||||
| |
|
||||
| $ mkdir -p .cursor/rules |
|
||||
| |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- We maken een nieuw project. Next.js 16, de nieuwste versie.
|
||||
- Iedereen doet dit samen. Als je vast zit, steek je hand op.
|
||||
- Na het project openen we het in Cursor.
|
||||
|
||||
> **CHECKPOINT SETUP PROJECT**
|
||||
> Iedereen moet `linkvault` open hebben in Cursor voordat we verder gaan.
|
||||
|
||||
---
|
||||
|
||||
### Slide 11: Setup -- .cursor/rules
|
||||
**Timing:** 09:50 - 09:55 (5 min -- samen doen)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| SETUP: .cursor/rules (3 bestanden) |
|
||||
| |
|
||||
| .cursor/ |
|
||||
| rules/ |
|
||||
| project.mdc <- altijd actief |
|
||||
| components.mdc <- bij .tsx bestanden |
|
||||
| styles.mdc <- bij .css en .tsx bestanden |
|
||||
+========================================================+
|
||||
|
||||
BESTAND 1: .cursor/rules/project.mdc
|
||||
+------------------------------------------------------+
|
||||
| --- |
|
||||
| description: General project rules for LinkVault |
|
||||
| alwaysApply: true |
|
||||
| --- |
|
||||
| |
|
||||
| # LinkVault Project Rules |
|
||||
| |
|
||||
| - Tech stack: Next.js 16, TypeScript, Tailwind CSS |
|
||||
| - Use App Router (src/app/) |
|
||||
| - All components in src/components/ |
|
||||
| - Use English for code, Dutch for UI text |
|
||||
| - Prefer server components unless interactivity |
|
||||
| is needed |
|
||||
+------------------------------------------------------+
|
||||
|
||||
BESTAND 2: .cursor/rules/components.mdc
|
||||
+------------------------------------------------------+
|
||||
| --- |
|
||||
| description: Rules for TSX component files |
|
||||
| globs: "src/components/**/*.tsx" |
|
||||
| --- |
|
||||
| |
|
||||
| # Component Rules |
|
||||
| |
|
||||
| - Functional components only (no class components) |
|
||||
| - Define Props interface above the component |
|
||||
| - Use PascalCase for component names |
|
||||
| - Export as named export, not default |
|
||||
| - Keep components under 80 lines |
|
||||
+------------------------------------------------------+
|
||||
|
||||
BESTAND 3: .cursor/rules/styles.mdc
|
||||
+------------------------------------------------------+
|
||||
| --- |
|
||||
| description: Tailwind and styling rules |
|
||||
| globs: "**/*.css,**/*.tsx" |
|
||||
| --- |
|
||||
| |
|
||||
| # Styling Rules |
|
||||
| |
|
||||
| - Tailwind utility-first: use className, not |
|
||||
| inline styles |
|
||||
| - No custom CSS unless absolutely necessary |
|
||||
| - Color scheme: slate-50 bg, blue-600 primary, |
|
||||
| slate-800 text |
|
||||
| - Responsive: mobile-first with sm:/md:/lg: |
|
||||
| - Dark mode: use dark: variant where applicable |
|
||||
+------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Maak deze drie bestanden aan in `.cursor/rules/`.
|
||||
- Let op de YAML frontmatter bovenaan (tussen de `---`).
|
||||
- `project.mdc` is altijd actief door `alwaysApply: true`.
|
||||
- `components.mdc` wordt automatisch geladen als je in `.tsx` bestanden werkt.
|
||||
- `styles.mdc` geldt voor zowel `.css` als `.tsx` bestanden.
|
||||
- Houd regels kort en concreet. Geen lange verhalen.
|
||||
|
||||
> **CHECKPOINT RULES**
|
||||
> Iedereen opent `.cursor/rules/` en laat zien: 3 bestanden met YAML frontmatter.
|
||||
|
||||
---
|
||||
|
||||
### Slide 12: Plan Mode Demo
|
||||
**Timing:** 09:55 - 10:15 (20 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| PLAN MODE: LINKVAULT PLANNEN |
|
||||
| |
|
||||
| STAP 1: Open een NIEUWE chat in Cursor |
|
||||
| STAP 2: Druk Shift+Tab (Plan Mode) |
|
||||
| STAP 3: Gebruik deze prompt: |
|
||||
| |
|
||||
| +--------------------------------------------------+ |
|
||||
| | "Plan een bookmark manager app genaamd LinkVault. | |
|
||||
| | Features: | |
|
||||
| | - Bookmarks toevoegen (URL + titel + tags) | |
|
||||
| | - Bookmarks weergeven in een lijst | |
|
||||
| | - Filteren op tags | |
|
||||
| | - Bookmarks verwijderen | |
|
||||
| | - In-memory opslag (geen database) | |
|
||||
| | Maak een gedetailleerd implementatieplan." | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| WAT TE VERWACHTEN: |
|
||||
| - Cursor maakt een markdown plan |
|
||||
| - Bestandsstructuur, componenten, types |
|
||||
| - GEEN code -- alleen het plan |
|
||||
| |
|
||||
| HOE TE REVIEWEN: |
|
||||
| - Klopt de bestandsstructuur? |
|
||||
| - Zijn alle features meegenomen? |
|
||||
| - Past het bij onze rules? |
|
||||
| - Pas het plan AAN als iets niet klopt |
|
||||
| |
|
||||
| NA REVIEW: Tab terug -> Agent Mode -> "Voer het |
|
||||
| plan uit" |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- We doen dit samen. Ik laat het voor zien, jullie volgen mee.
|
||||
- Let op hoe Cursor nadenkt ZONDER code te schrijven.
|
||||
- Dit is het moment om het plan aan te passen. Straks is het te laat.
|
||||
- Na review schakelen we naar Agent Mode om het plan uit te laten voeren.
|
||||
- Cursor bouwt dan de basis: types, componenten, pagina.
|
||||
|
||||
> **CHECKPOINT PLAN MODE**
|
||||
> Iedereen heeft een plan gereviewed en de basis van LinkVault draait lokaal.
|
||||
> Test: `npm run dev` -> open `localhost:3000` -> je ziet de app.
|
||||
|
||||
---
|
||||
|
||||
### Slide 13: Agent Mode -- Features bouwen
|
||||
**Timing:** 10:30 - 11:05 (35 min, na pauze)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| AGENT MODE: FEATURES BOUWEN |
|
||||
| |
|
||||
| We bouwen nu extra features met Agent Mode. |
|
||||
| Start ELKE feature met een NIEUWE CHAT. |
|
||||
| |
|
||||
| FEATURE 1: Edit Bookmark |
|
||||
| +--------------------------------------------------+ |
|
||||
| | "Voeg een edit-functie toe aan bookmarks. | |
|
||||
| | Een gebruiker moet de titel, URL en tags van | |
|
||||
| | een bestaande bookmark kunnen wijzigen. | |
|
||||
| | Gebruik een modal of inline edit." | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| FEATURE 2: Tag Filtering |
|
||||
| +--------------------------------------------------+ |
|
||||
| | "Voeg tag filtering toe. Toon alle unieke tags | |
|
||||
| | als klikbare chips boven de lijst. Als een tag | |
|
||||
| | geselecteerd is, toon alleen bookmarks met die | |
|
||||
| | tag. Toon ook een 'Alles' optie." | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| ITEREREN: |
|
||||
| - Niet tevreden? Geef feedback in DEZELFDE chat |
|
||||
| - "Maak de chips kleiner" |
|
||||
| - "Voeg een hover effect toe" |
|
||||
| - "De filter werkt niet als er spaties in tags staan" |
|
||||
| |
|
||||
| NIEUWE FEATURE = NIEUWE CHAT (context management!) |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Na de pauze gaan we bouwen.
|
||||
- Elke feature in een nieuwe chat -- dit is belangrijk voor context management.
|
||||
- Ik laat Feature 1 voor zien, dan doen jullie Feature 2 zelf.
|
||||
- Itereer in dezelfde chat als je kleine aanpassingen wilt.
|
||||
- Niet tevreden met het resultaat? Geef specifieke feedback.
|
||||
|
||||
---
|
||||
|
||||
### Slide 14: Debug Mode -- Tim's Bug
|
||||
**Timing:** 11:05 - 11:25 (20 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| DEBUG MODE: TIM'S BUG |
|
||||
| |
|
||||
| Tim heeft HANDMATIG een bug geintroduceerd in de |
|
||||
| filter-logica. |
|
||||
| |
|
||||
| DE BUG: In het filter-component is |
|
||||
| tag |
|
||||
| veranderd naar |
|
||||
| tag.toUpperCase() |
|
||||
| in de vergelijking. |
|
||||
| |
|
||||
| EFFECT: Tags worden opgeslagen als "react" maar |
|
||||
| gefilterd als "REACT" -> geen match -> filter "werkt |
|
||||
| niet". |
|
||||
| |
|
||||
| +--------------------------------------------------+ |
|
||||
| | STAP 1: Reproduceer | |
|
||||
| | Voeg bookmark toe met tag "react" | |
|
||||
| | Klik op filter "react" | |
|
||||
| | -> Geen resultaten! | |
|
||||
| | | |
|
||||
| | STAP 2: Debug prompt | |
|
||||
| | "De tag filter werkt niet. Als ik klik op | |
|
||||
| | de tag 'react' worden geen bookmarks | |
|
||||
| | getoond, terwijl er bookmarks zijn met die | |
|
||||
| | tag. Onderzoek de filter-logica." | |
|
||||
| | | |
|
||||
| | STAP 3: Volg Cursor's analyse | |
|
||||
| | - Cursor onderzoekt het filter-component | |
|
||||
| | - Vindt de toUpperCase() mismatch | |
|
||||
| | - Stelt een fix voor | |
|
||||
| | | |
|
||||
| | STAP 4: Review de fix | |
|
||||
| | Klopt de fix? Accept of pas aan. | |
|
||||
| +--------------------------------------------------+ |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst (voor Tim):**
|
||||
- Voordat jullie gaan debuggen, introduceer ik even een bug.
|
||||
- Ik verander in het filter-component `tag` naar `tag.toUpperCase()`.
|
||||
- Dit is een realistische bug: case-sensitivity in vergelijkingen.
|
||||
- Jullie taak: gebruik Debug Mode om de bug te vinden en te fixen.
|
||||
- Geef Cursor goede context: wat werkt niet, wat verwacht je, wat gebeurt er?
|
||||
|
||||
> **CHECKPOINT DEBUG**
|
||||
> Iedereen heeft de bug gevonden en gefixt. De filter werkt weer.
|
||||
|
||||
---
|
||||
|
||||
### Slide 15: Code Review & Tests
|
||||
**Timing:** 11:25 - 11:40 (15 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| CODE REVIEW & TESTS |
|
||||
| |
|
||||
| STAP 1: Find Issues |
|
||||
| +--------------------------------------------------+ |
|
||||
| | Gebruik Cursor's "Find Issues" feature | |
|
||||
| | Cursor scant je code en rapporteert problemen | |
|
||||
| | -> Fix wat je vindt | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| STAP 2: Tests schrijven (nieuwe chat!) |
|
||||
| +--------------------------------------------------+ |
|
||||
| | "Schrijf unit tests voor de bookmark filter | |
|
||||
| | logica. Test dat: | |
|
||||
| | - Filteren op een bestaande tag werkt | |
|
||||
| | - 'Alles' toont alle bookmarks | |
|
||||
| | - Case-insensitive filtering werkt" | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| STAP 3: Semantic Commits |
|
||||
| +--------------------------------------------------+ |
|
||||
| | Nu pas committen! Splits je werk in logische | |
|
||||
| | commits met semantic prefixes: | |
|
||||
| | | |
|
||||
| | $ git add src/types/ | |
|
||||
| | $ git commit -m "feat: add bookmark types | |
|
||||
| | and interfaces" | |
|
||||
| | | |
|
||||
| | $ git add src/components/BookmarkForm.tsx | |
|
||||
| | $ git commit -m "feat: add bookmark creation | |
|
||||
| | form with tag support" | |
|
||||
| | | |
|
||||
| | $ git add src/components/TagFilter.tsx | |
|
||||
| | $ git commit -m "feat: add tag filtering | |
|
||||
| | with chip-based UI" | |
|
||||
| | | |
|
||||
| | $ git add src/components/__tests__/ | |
|
||||
| | $ git commit -m "test: add unit tests for | |
|
||||
| | tag filter logic" | |
|
||||
| | | |
|
||||
| | $ git add src/components/TagFilter.tsx | |
|
||||
| | $ git commit -m "fix: resolve case-sensitivity | |
|
||||
| | bug in tag filtering" | |
|
||||
| +--------------------------------------------------+ |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Nu gaan we ons werk opschonen.
|
||||
- Eerst: Find Issues -- laat Cursor je code scannen.
|
||||
- Dan: tests schrijven voor de filter-logica.
|
||||
- Tot slot: alles committen. NIET alles in een commit!
|
||||
- We splitsen in logische, semantische commits.
|
||||
- Dit is hoe het in de echte wereld werkt: je bouwt eerst, dan maak je de history netjes.
|
||||
|
||||
---
|
||||
|
||||
### Slide 16: Git Push + PR
|
||||
**Timing:** 11:40 - 11:55 (15 min, waarvan 10 min optioneel)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| GIT PUSH + PR |
|
||||
| |
|
||||
| STAP 1: Push naar GitHub |
|
||||
| +--------------------------------------------------+ |
|
||||
| | $ git remote add origin | |
|
||||
| | https://github.com/[jouw-username]/linkvault | |
|
||||
| | $ git push -u origin main | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| STAP 2: Maak een PR (als je op een branch werkt) |
|
||||
| +--------------------------------------------------+ |
|
||||
| | $ git checkout -b feature/linkvault-v1 | |
|
||||
| | $ git push -u origin feature/linkvault-v1 | |
|
||||
| | -> Open GitHub -> Create Pull Request | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| ============= OPTIONEEL (als tijd het toelaat) ====== |
|
||||
| |
|
||||
| STAP 3: PR Review met Cursor |
|
||||
| +--------------------------------------------------+ |
|
||||
| | Cursor kan PRs reviewen als code reviewer. | |
|
||||
| | Open de PR URL in Cursor en vraag: | |
|
||||
| | "Review deze PR. Focus op code kwaliteit, | |
|
||||
| | mogelijke bugs, en best practices." | |
|
||||
| | | |
|
||||
| | Dit is OPTIONEEL. Alleen doen als er tijd is. | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Push je werk naar GitHub. Maak een repo aan als je die nog niet hebt.
|
||||
- Als we tijd over hebben, laat ik zien hoe Cursor een PR kan reviewen.
|
||||
- Dit is optioneel -- het belangrijkste is dat je code gepusht is.
|
||||
|
||||
---
|
||||
|
||||
### Slide 17: Samenvatting
|
||||
**Timing:** 11:55 - 11:58 (3 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| SAMENVATTING: 5 KEY TAKEAWAYS |
|
||||
| |
|
||||
| +--------------------------------------------------+ |
|
||||
| | | |
|
||||
| | 1. AGENT HARNESS | |
|
||||
| | Instructions + Tools + Model = Agent | |
|
||||
| | Jij controleert de instructions (rules) | |
|
||||
| | | |
|
||||
| | 2. RULES MINIMAAL HOUDEN | |
|
||||
| | 3 bestanden, YAML frontmatter, korte regels | |
|
||||
| | Te veel regels = slechtere output | |
|
||||
| | | |
|
||||
| | 3. PLAN VOOR JE BOUWT | |
|
||||
| | Shift+Tab -> Plan Mode -> review -> build | |
|
||||
| | Elke feature begint met een plan | |
|
||||
| | | |
|
||||
| | 4. CONTEXT MANAGEMENT | |
|
||||
| | Nieuwe feature = nieuwe chat | |
|
||||
| | Geef relevante @ mentions mee | |
|
||||
| | | |
|
||||
| | 5. SYSTEMATISCH DEBUGGEN | |
|
||||
| | Hypothese -> logging -> reproduce -> | |
|
||||
| | analyze -> targeted fix | |
|
||||
| | | |
|
||||
| +--------------------------------------------------+ |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Dit zijn de vijf dingen die ik wil dat jullie onthouden.
|
||||
- Het gaat niet om welk model je gebruikt, maar hoe je de agent aanstuurt.
|
||||
- Goede rules, goede plannen, goede context = goede output.
|
||||
|
||||
---
|
||||
|
||||
### Slide 18: Huiswerk
|
||||
**Timing:** 11:58 - 12:00 (2 min)
|
||||
|
||||
```
|
||||
+========================================================+
|
||||
| HUISWERK |
|
||||
| |
|
||||
| Zie: Les09-Huiswerk.md |
|
||||
| |
|
||||
| +--------------------------------------------------+ |
|
||||
| | De opdracht staat volledig beschreven in | |
|
||||
| | Les09-Huiswerk.md. | |
|
||||
| | | |
|
||||
| | Kort samengevat: | |
|
||||
| | - Breid LinkVault uit met eigen features | |
|
||||
| | - Gebruik Plan Mode + Agent Mode + Debug Mode | |
|
||||
| | - Maak nette semantic commits | |
|
||||
| | - Push naar GitHub | |
|
||||
| | | |
|
||||
| | Deadline en inleverinstructies staan in het | |
|
||||
| | huiswerkbestand. | |
|
||||
| +--------------------------------------------------+ |
|
||||
| |
|
||||
| VRAGEN? Stel ze nu of via Teams. |
|
||||
| |
|
||||
| Volgende les: we gaan verder met AI-assisted |
|
||||
| development workflows. |
|
||||
| |
|
||||
+========================================================+
|
||||
```
|
||||
|
||||
**Spreektekst:**
|
||||
- Het huiswerk staat in `Les09-Huiswerk.md`. Lees dat bestand goed door.
|
||||
- Jullie gaan LinkVault uitbreiden met eigen features.
|
||||
- Gebruik alles wat je vandaag geleerd hebt: Plan Mode, Agent Mode, Debug Mode.
|
||||
- Vragen? Nu stellen of via Teams.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Veelvoorkomende problemen tijdens de les
|
||||
|
||||
| Probleem | Oorzaak | Oplossing |
|
||||
|----------|---------|-----------|
|
||||
| `npx create-next-app` faalt | Node.js niet geinstalleerd of verouderd | `node -v` checken, minimaal v18. Installeer via nodejs.org |
|
||||
| Cursor herkent .mdc bestanden niet | Verkeerde directory structuur | Check: `.cursor/rules/project.mdc` (niet `.cursor/project.mdc`) |
|
||||
| Plan Mode doet niets | Verkeerde modus | Check linksboven in chat: moet "Plan" zeggen, niet "Agent" |
|
||||
| Agent Mode maakt geen bestanden | Permissions of path probleem | Check dat je in de juiste directory bent (`pwd` in terminal) |
|
||||
| Filter bug is al gefixt door Cursor | Cursor heeft het vanzelf opgelost | Tim moet de bug opnieuw introduceren NA de build stap |
|
||||
| `npm run dev` geeft errors | Ontbrekende dependencies | `npm install` opnieuw draaien |
|
||||
| Git push faalt | Geen remote of geen rechten | `git remote -v` checken, GitHub repo aanmaken |
|
||||
| Semantic commits zijn verwarrend | Studenten weten niet wat waar hoort | Laat de voorbeelden van slide 15 zien, doe het samen |
|
||||
| Context window vol | Te lang in dezelfde chat gebleven | Nieuwe chat starten, relevante bestanden opnieuw @-mentionen |
|
||||
| Cursor genereert andere code dan verwacht | Model-variatie is normaal | Dit is OK -- focus op het PROCES, niet op identieke output |
|
||||
|
||||
### Tim's Bug -- exacte stappen
|
||||
|
||||
1. Wacht tot ALLE features gebouwd zijn (na slide 13)
|
||||
2. Open het tag filter component (waarschijnlijk `src/components/TagFilter.tsx` of vergelijkbaar)
|
||||
3. Zoek de filter-logica waar tags vergeleken worden
|
||||
4. Verander `tag` naar `tag.toUpperCase()` in de vergelijking (bijv. in een `.filter()` of `.includes()` call)
|
||||
5. Sla op, NIET committen
|
||||
6. Laat studenten de bug vinden met Debug Mode
|
||||
|
||||
---
|
||||
|
||||
## Timing-samenvatting (verificatie)
|
||||
|
||||
| Blok | Start | Eind | Duur | Inhoud |
|
||||
|------|-------|------|------|--------|
|
||||
| 1 | 09:00 | 09:10 | 10 min | Welkom + terugblik (slides 1-2) |
|
||||
| 2 | 09:10 | 09:35 | 25 min | Theorie (slides 3-9) |
|
||||
| 3 | 09:35 | 09:55 | 20 min | Setup project + rules (slides 10-11) |
|
||||
| 4 | 09:55 | 10:15 | 20 min | Plan Mode demo (slide 12) |
|
||||
| -- | 10:15 | 10:30 | 15 min | PAUZE |
|
||||
| 5 | 10:30 | 11:05 | 35 min | Agent Mode features (slide 13) |
|
||||
| 6 | 11:05 | 11:25 | 20 min | Debug Mode (slide 14) |
|
||||
| 7 | 11:25 | 11:45 | 20 min | Code Review + commits (slides 15-16) |
|
||||
| 8 | 11:45 | 11:55 | 10 min | (Optioneel) PR review (slide 16) |
|
||||
| 9 | 11:55 | 12:00 | 5 min | Afsluiting + huiswerk (slides 17-18) |
|
||||
| **Totaal** | **09:00** | **12:00** | **180 min** | |
|
||||
|
||||
---
|
||||
|
||||
## Checklist voor Tim (voor de les)
|
||||
|
||||
- [ ] Next.js 16 beschikbaar (`npx create-next-app@latest` geeft v16)
|
||||
- [ ] Cursor werkt op docentlaptop
|
||||
- [ ] Drie `.mdc` bestanden voorbereid (kopieer van slide 11)
|
||||
- [ ] Git/GitHub werkt
|
||||
- [ ] Weet waar de bug geintroduceerd moet worden (filter component)
|
||||
- [ ] `Les09-Huiswerk.md` is klaar en beschikbaar voor studenten
|
||||
- [ ] Backup plan als Cursor offline is: theorie doornemen + pair programming
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user