From 608348a5a56f560f19275ca493584fcb92f69e1c Mon Sep 17 00:00:00 2001 From: badbl0cks <4161747+badbl0cks@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:10:33 -0800 Subject: [PATCH] More progress but need to fully refactor into Astro action and take advantage of all the boilerplate --- astro.config.mjs | 10 +- bun.lockb | Bin 206911 -> 213390 bytes package.json | 1 + src/components/ContactForm.astro | 73 +++----- src/pages/contact.astro | 158 ++++++++++++++++- src/pages/endpoints/contact.ts | 295 ++++++++++++++++++++++--------- src/types/ContactForm.ts | 16 +- 7 files changed, 411 insertions(+), 142 deletions(-) diff --git a/astro.config.mjs b/astro.config.mjs index 5103b88..e98b414 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,14 +4,20 @@ import { defineConfig, envField } from "astro/config"; import alpinejs from "@astrojs/alpinejs"; import sitemap from "@astrojs/sitemap"; import bun from "@nurodev/astro-bun"; +import node from "@astrojs/node"; import db from "@astrojs/db"; // https://astro.build/config export default defineConfig({ site: "https://badblocks.dev", trailingSlash: "never", - adapter: bun(), - output: "static", + // bun adapter is not official, so keep + // the node adapter available just in case + adapter: node({ + mode: "standalone", + }), + // adapter: bun(), + // output: "static", devToolbar: { enabled: false }, prefetch: { prefetchAll: true, diff --git a/bun.lockb b/bun.lockb index 7c45111227f8bf745d4c3f61af8fa96800795885..41a9d0fbd72a4db930404e983641858fe1b1b73a 100755 GIT binary patch delta 34564 zcmeHwcUVtMWq}BrAe_X$KJ5uR!7B#J@(#9 zu*VW(5{+15jGCB46OD-_w)?I7et1t>oTd|BX4U?pG*urly6%9DII zYWWD5keNKvJ2idq>D;@H*-l1Da)7V|kP1Dv<%Mnm%Y*L^%jJNX8Cu=aDaf6`+XAZt zHvwIM-jPNp8S}Ha&qe)SMGjibe)DF zMPRyluz4ix7{OCzuThTj9|OtWw?JylZ)hooG21eFL}F%af+U^BAW-n6!gtWI^afIe z+aafM$^(+SX9&Fvcf=+5L?8uO7a-bX$-`ii+ouXl0g|09HF^0!=+SW02bPtxCCdn5 zFjiC$2BZpOV^gzIG9_sgYNdwRdU8w$Qjg++dTwLo;zG6G0F9snfIZfL~qP6cL@!Vn?o4kX290^NWVP;us%tic%5IR<^Gmw?;HopFVn;;Nj$6=8hXN(lZXI!|qTEbtzXydNKzm`

DQ$FNHiAEXcY3DkW>Cf zQC<}on-HBe6oWid@D?DI*X>6oakmQlrL7gH`K4=TFH7O-DbXpZiE(IbYEn{Gnk3yC z##g4}z^X8}21qN<44?}zNyx*6JP=50uQRY3@HK1@-T=}PdrqzH&kN=r;J56PG~p3mzqfaLo%$S()X1yUsF zi_y|-9*XJa*yxN*jD>|;w*YA@%o*6yBu7glz*B3|qB9c^5Yi+NwGu%>m~Dwgf(+l7 z$er;iusrx9&?G}!fz*J#kW;MBpUm^ufwu>*t;w*_9Yp-XDZF7B2*u>+G)bR%&8KpE zSiYetNi9JYXs_Toje8(2HPf6jT#_0igI2PV(|JoX%qa*GNy^BK&dka%XB>f?qIf5e zJY+U|#V4j`WNrXY4Ngu>HhaazNXtZi)=aKH7)U)&Oi3`OCuU~EnbYD>za+({n==xC z=FDiVX}O}{bRfB|F7l|N+#GJ8o%*^uH_;Q&9o+aGa=6pPA^5=CV;3G^!(HF*nccE)jx)z5C1xceUtehS1*BjJ?~wgc-jGo~K`Bs^h9;S_(t-CG{7$YiEaR?QDNqGc z!=B~wmabgRd(d$O*SiFy{F6ZHp+As3a%Ux#W{dz*xykEzb>9Ig=F@;QKzg&x$;swS z^iSEqSE$G9HT`T$6covT{YGBFpHQF*;$fIZ#`$Bu)J1})c83FLgabD5e#WHZqK1*! zA^7LOkC5MfGuKNtrw=y!N>Y8uX+W`u7@U+EBl&Hi@|bL{$e+J;_FF7=>_S0{Ub>adpJ#4yR+rzhaT~GWzd8quf~1nFk|n8)TGc(+U{tWOsmp6LQwt$q4X%kMzll^QE#+B4 zlENru%s{FId{IKZ>mFz<02d5SQKRbz%5T)jnkKoix&(jI)I$87q&n9!85&@8JF8V| z1sg^p)kRC4K`LBJRfap;s!_F?sgWKg`D1m7hskggvL2KxH&mTHO@{dx=RTVDPe=_` ztJY~|gQ>xdE=Q`LTGcby@DeF*v^@fApeEaZR1Y<(ZZp|Vb@2=_3_vJxooz^Q9Xmt= zmqj7PWk-qE*e1Z+jk!nUMxlncXH5u();dzuPt+{0oIE)o? zPpXB;ty2Y|fn461ae{;W27$(t;IIf#*Cqz3g?=VO2=-GUS}kLd(rPgrK&mOpj3uf` zQb*_;)o8T*YjD&nMRoTLG?u}Js53YljhhaRh6)aao4y0rN7JMpH?58qat;oi1CCl> zio79zqb>qWY=OB~u6UfM} z=1HS{ElKJM4n9Y}2ZE#F=51XJj#|#!dLJB><6L78y{$Al)4_=zqYX#E@p3ecT|7Bg zQq4ynCx9cbBiPXYv*4&cKE_gQZUtUMUN|@kD6Y8}91k1Hy9X`=d8IVX9(9WQbQ?Gd zJzmbSE)P;g&36y71=AW?WmI<Pym~BJLcx z1aPv}p;ire_qaRffSajlQrDfl5Fp?X>)>92qlQ5fj_B#FJCJJl7#z~2T4{~wwsK3r#ppH2uT|$(w2nvOZ=|}U zmC3lwPm=IYwEQxX6bRhGZW#Aw8b@uN2d)D+&8LQ6!C|GXA8d&8r-kfx!(ihsq(Y&A zDGx6^2S@GTK~*(Sk}|<@59Na6UcmSmUW1EKs|E!dqk?c%Ez-ISjz)u*2}CrFEaJ9< zv(_~nL&1AV<9!_*p9W+-02i82wW@EhaWqogYov7p9QPBowh=lSuUU_d39bQ)4!xrlDlEv> z#HYTkW^xQTn&!6J;`jg@8A0WkjCC$>r&pjy6 zm<*1p<4suyj+Po5Ey|yP>!@*eG0O31Kh@4VdjOnuVX(ynqjEexdx7KKBX2GR$0LW9 zf}aE@Ywc_bHCh-9TGzLL<1>eP^gTFo8J}Zy+VFjWtVY)ilw(!rt|s}g8rjuk{24ju zAclluu|CXE@Axvj6CCBCU1-Tu!NG=CpzNbA2{*~nY9apaQ=KDB#wYFg0>cN_yFG6k zqO?_@F%ul7E1&;{)!=+7#^tZn$ZjUZwu8E~TZkbP!{1uH-2hi5q*|#_jhfjaL4DF< zU3ONNbT=8h!l^Xi+H5c^0@sDE-12=jvWLm&&`GZe^~$Z(LdYgVM*Yw(lJY4vvZu*V z8mnz6%`Ul%TG-R1RX(n2f`4&7i($YaqPu9BG2d$Vii6W(0bq zR*eXjkEw-`CZjC^#TT0JJI1^zI7}&;80b-!x}>kkcos5>5ySyqF1zboen6nyRgLUt zGR}aEB2yy&%LQs7WG^5i&!H5?vp#~l5jezkk3eHTaHNlzM~BoRjx;X_j*n&ep1d|~ zOpHCjQLk{Z=pJa8OPm_jAlUe+kn{R0sF4FqhK^Vkg4EmCwN9iIN=d4FM4<6oaO8A& z2JNq*Mh-L?mh{HDp+*f1mfx#|;FJ1jTZns-^ZI6{e-+L3*l7Q7W$}^{^=AA5FIgWW`dHx`0#%P391zr=p5d>x)vq7Nvq> zb-f8isq;mtigA+EPP5myD3xE7dRUZdXr>#PsDx%VT7nvNuqgGeDAhWi)R1eOjug2C zYhz@f@ff(qx|58x2TPJaI6G}WkN~a)IKEDA1V@^D_5EIOJT6^ufwNla0*=O=UkOfw zqp`*11Z$o{BDYzJR&6;$Elf5UPeNvbMrkd^6kJmI#fWA@d%+a>Po0vD#5j2nau7bBRw3yvz_i*VH>-XLs&U@{Y2FD;KW z&x51tcsbi-UXI^RgbI!V7^~1QEEACMDSkIR(CD4QBNEF>w?H{wU6O7xu7`|j<`MZs zbfnl;mt6*ql(H1HbkY<{T8JTAQh>l%4bm2k}Mu6o?C!aq`Ew-nN6mI`&Y_L zMk-26oi9q&&XS~9O*RuLUd7Kvsm{ao5+5TqSkto~f#+meDhsKBTI#E!l;23b#5|;U z^L|8%>$M-H%kq)xO=XRS(cCTk`raO#H+KQ9RKrCcMiA@8dT>F=!|H~W=^NE~w8`i= zhRgW)cLhfdgg&BSg_Xl9`Vd^GRvKFqLxZujHexqsh({`ju35$vl!69A1EueSHr`L^nuZIWB6k=Z80Vhh@%Ms*7|-a@Jw zbg<@PhPIi&=PPb`+ymt?YNW+v+y@!WFzwnRKU51L^O(p>p$C{*(Q4!b6P`&d!QVu+ zaDvHrW)jcW)^4LRneP~20e9r>!F5z`PY71lOjd792r>Qu0ZmCX0nPMOBPW@Ry{2#( zx{FJ}CUD)L!M7`=rt(JG(j=GLsgaXSvPE4o*<{>{9O^K(O_=`oz_sS3dIZYd)yOF( zvzV}TeD zsC>-SrL#knH%z@TJH*g+F!Sgzgxm{+JS^RZiA!NU;kgrl8T^7^zz$v7KaH`JhA zsts?!nboSf!N#;zJXme1Z_1uk>QXht@EpPx+Vi)z`Krr;5M_M6+IK;SaZi3x^m+s; z_w&^o3qlM{S7Su9F_?)|7p=cE->!q>Ub~A$(S8kHs8N^a1{-D}h3isoup+Hh`z{JG z3|gygxgaQ5OT99Dv`&jov~CGf9bl0^+xZO~tvQ%xzJZF*di4eiF^*o(JFV?_l|$>* zzKcVQt{V{PT1(vL2O5*Wkrj+)w?N}waBaXz+RSgT5wnAHXyqhui2R1ZhR=8kjq>|g zby*r>ocl2!OkV5?I2v{>42>-|aaUty#bAyFMlf0ioyt5xbnOONBc2Y_X;#L--5a*NR?4rRF~x;#tRTof3T8`3p7^Q z%6-Hmw;wpZFws)92^>W=yorV4$yT-RiV$PXZQQzcH)ear@t_#Q%kUI&DYcc@EOHKi}O=(`c^Ls$ajD9{C{ zfU1D#Qygi@tqLOj>LB9XLG&q(RK6yt1t=Ee3K|ch53qusC_-5sw{x0WcD1cDiKc7v zzoM*qt*)I-70wd5AHqt=TLmJ$d=PyIsX=Q5B51XI#HscnqX)mXBgpnX5Pd#`wvZnKkdAqB@IAoX)Pkg{e7eh!fAEC7<;G9Wc$ z6_5gOgWxxlU&z1?2mcD^>I}1o$UC!XGmHtKgrI5TD{m4f!2%GVomJy#P{=-wOQ?A(fM5J$b=-!A+Jvema{mrVr8@plBY$S;?H)R`-yN5#>gdL40W zLR!~FE+P4aUTh%zM#w*iYVsCGA1ZxUXcb2)eP754iT?p;P%mw9)9QRIazBK$@cl0G z390l8;?=50`4-`o$R(uQH-aamkkf<$sf{uz2_)4L;7M*H7WgygxPY$T}1O@tsA zh<{Rukeh_OB?45 zWOxIR`n?TE2Dbz8Puhh)WMChV8h98;`9~;IAjt~^Pe}Go3cXVTPYd~3AbrkKMDYZ# z-~uwJqRT>okPLqXB!kz0RPomWuL~>$(x*6*-VKp|Q{)qpo!bKMX#R)bj!^toC=!x^ zyMiaAhCKk%_Tm>IC!}57?}8_!3Z4V0{0o6dYImXkdMBFwKX@xjZ76< zA}wFYKZNAZV)vlnH2(khd(lC{W&h9aMRyl|(qmSOXDgq+?2(pqWzx*$-7mT4%=kF) z>vyw`IJJ1XdDDo(POFCJJQ(Jo{ycr`%op{1haCLmVNO`NeKqc1>t5j#)1VQ+z$d7Rl8+Ur!6!S=eoL*7{@V>tzpRL-(20z76{%&?X{q#V+;r(yNvkT_c}= z_0<>8KAM>JgNL-H>#dhj#`MK|5hJqnuG~8yqj(V?n`_3x>>a1mCsrBK_+D4rCkKL_ z9S$ki{7gW{FXk>Pu(Tey*e%(%P2Ym-#LPuYu0JgB9ewyz|1Y~g9CUYD)U!X9Omp@x zW_WRN!yi5M{bAvVvekZ>pPBG_`W)wwfu?>Q?JnA{t`s$YJAP417u3Woqsuc-slGabFxFO)h z>hV<@*$>DGb=+uoXhD~y(KFiS>>itZE3M2o8>uTRv?I*{t+;JNYLvU{6rerddRYRz})%@Ss>Skrg?QMtyLDXp&= zlRBkjt{l*5=TN6T^;gZP-;KU<(_>0w_wva>mG5kR8Z^j1aox7YHTU-jHoJ|#)TIBI)Fv;gmF_V5 zNsTirx~zIwH`}i;rKXE@bcFLuKNk+Pr)m@xl+_3ukOyYP`Ot1a@n&p)K{qrWnt5qNKQ_$wY8y!vk%`b zKNqyob2|R_$xxShTYJoyF;%_)?vQfn+KaK%M}Pg=wCuH8yG^yvU94qmlif6Y|2g|T z3qrSdn{1<8u-#s)c@$>)$EKazwNKBC{ybyp#$A?K8%l+)p8jjIUP;UU@O6H?^j6vY zwcj}IJ?(Zq+PUP)hVB75GroQ>+o9>GDHH3pRqH=m+ave#b=Eur%}bd0%CXw_Lk)fJ zgt@RmgYCQ22>8=~W9U=w-RIuTZqo3`pA+ZYDPz~+VW?-)HpAlOmQDw&_ZWD-{K9Qx z9dDQJUWL9>*S)u{c=G~kZCkhEqtSPFY~SwTG;LSjs|N0>G5>1GCe@d`>9L`!WBt)> zf8BJTs&jYe4ngN*X0Kk-tC4Z*kTbjA&A4zf=Ww03YuIBnuXF1TZ>u^T$XlJ_G$m_f zWc-nNW8&HjJAC`xmW!)>3nqTi_hPNnyW5r>-Dk+QPk*m&?)Bo@%eE6n-C6uBY(UcB zrlBpj6>HuG=H6NE%{F$E?bwvgawqv?b_D`?6RR5r*vuvnY+;22TbWN6z&196U^}}@ zu!A+}3fReV33jm`33juV;V^ry7tF2;huOXCDG5gQhM;Ez1p8Tj1O#>ZKw#(w!9f<$ z4T9?=*h_-LOzsZB^hgNI-61&2c9I~dF9el(KyaKz_kiFb2~LxsfR*nF!Qy@pWc7sL zBr706`~DDk^n&0t8`cYgmn67Kg0sxMHv}7_Aehn{f=}5M67(4Wfqx$eF0cuGAg~<> z!2=RpVm^@&93a8MNC+;oyCg^$1VNj=5L{uoeIam)hTsnpe92n&gWwzq*7bwnDtk(T zkuebT><_`$EWbYlbz&hfL_tu@EoshCDYlgrn z34%XJ@Qk&Lhu|Cu*2P2c2YX6_k;xGB91Ot=mOmJRIw=qs5+HcRA`&3DPJ+E8c+KQQ z2&Sh(U`~YKE!#(zVW`|oQ4}_Wu!O=+5ZWlrB?)Ly zSUO=zg`FoXrLY>wKwE{4CNwJS3&PS0tCs>Sqp*pDWfgXV&`x2#slbmEHj}WN!oDN4 zS6I_DV0nej2g()p|ClDvEnz6Yn>ltYdY;^U|Ej5SALZZwGRQ}&pI5hGTgJ<_hPo~B z8b?t9TZMAuY0P(2(vlY5b|Dq2PC&2%*3)?oaP`Jw_Xc!5_5 z&)rLZUF0(J?5JM+{vF18`fItF*zmo3&p(v)UR!Uq-x(mbh^K;8#9p5673oSr?4R7C z_J$A1=r*V?h(7weJ@jRl_#Rz*&xiEr`=wa@iz*vokREe*h)N7XMqk>_K$?t{1md6e zWz{T7;A0CUgQY<^LPkF@KuP*u_kxg>0g@hlCrR($&?mdB5YkIj%Y~49M2a7Qz7&n2 zw~XdqvC&Hx)OdRHh|1Cn6AO^0Z`vvX@h_WxE<&1% z6t%t*kQC|bwAn&dS;*+UNO}i>dgLf%m62X38sj8n^vxSpOqJ1_O=OSWz|h}Fa1pW{ z)Os4MDo*U!TRA0y8baUi(YM9}Km$Q^@8%Aw3UULvf~tWi4yuFbyZDkI`m)~uQb6<& zhGOD18}?3i&3=X?y~l6|6oV=TfO>;4k?B93YN1jO*d7!CqAYIIM8?y%>@f+0%#(L-s_45#(@Td54DOIik^IHj1T*rcU^UJj%;m8OEGfu@6I zf@mhw%$DAlmdzarGgScBSB+8X`oS{VIVUo3p4~Y8k7zi z3yKG2fQEtwg9d@N zeF{1cx&XQess`!>>IwQ7dYeGB=6HZSLA613K=nYYk+%v+%TFH26;ur}?G;se=j{nb z>_^Z~pr1kX`oQO)E1)kxv__o+?FStM9RO_xZ3NL;H3c*cGz7$8Z!yRp>Bb;GP(4sx zP$Q5J$Q#rUlwBWx8i2e&bwKngPGyiI$Qk4SqW5@yfWe2LA3;BXeg=)FUV&c1#jikr zf_?)%1JTQ-r$Hw`37|yKP*8bL1&|#GuPRrTp1{C%&`!`U&~A`3d#fllv&*24`Jg!< zdQ-7G=oGTA0I!0+1bqeS0}2DR1GNFQ0<{Nq0EL3ug4Uwke&8<9PS7WyJ)o1I1E7PT zy`X)d!=Pgnn@5m13OWut1v(8n1KJHb1S$ZX0MS=#^oq+Z5WT2DFLQhaIt@AlIt)4j zItrp!X6A$DfaZc|abE%(fk1jh8w_d&YF?s+_BOk82cG&C^d0CPh~EGG6m$V}5p)^! zB@r6&73eCc5OhOg18kJa+3S%+bXh%v^jXk3&^7SaLATNPZ$b11%e4~lVgV8-K>47xpf#XWkO|ZR z^bi%@f`Qwh!$=<(0RZe*!vok208{= zD7^&T1_^o!)*jRuWCj^PB|-EaPHEtGXn9}Id(b=ZGk|o#uMNsYny$BWVW;ajUAO5v z-5In6bQL-(Xf`OB(vaz8a^1m@;4)~b$h?hodnf$Cr%oNZHrKE#50>0|291huw zZKr`HK&L>@!JhrAetDhK_n-=oz* z=n?2=^2AR_JOup!x(1>dHx;D!<|@*YK{Pu_PV_a1^6vp@_TB}33!*&Ir^;3fqE8gJ@)^?^F?0N<59y6d?7_4ry{=aUDIs9P)U%>O5pJ zp*R9m0T~pFedq#>&-A(MSS6(%dsInDaKucr=s&fl$OD>q`+H;a*pfw5R-EPTY$(7r z7KJK6Z_xYSkDskD!~i`XZ=Xiq0a6xQ2?hU2P{7TFrBdtnLF>!MErmiOZy#?zKW$e< zMZ*sn9&f#Pa2|4ek%KDmFs-uUhbLCWJ!NSb-BDg*_#1GAl!z z5Z+`aaIQ3h&agi%;jv{!qq9lWG+%F2E(yKblbM&9e^~*f$aHVqXSDJI+ z1b<2YG2KO>F7G-t*>hgY3Gns_l3uc86m`{qeRt%n%%c?_{W)8a{m?f*c)J|Shl1?N zejN%u{a1WXWOuondgt%}7!rz&*{@_s|M}l4FD+&E9N#hl3XL%gXk=IRxF!rmvF6pF zr~hp5PbC{19rDfn4ba0#lipZ13<`1%yBn=kVymhtHm>?F6T570xvZRB+Mm=qREySa zXUC!JUw}JY8vLNgO_xNKnpR$Gmajil^xsrI`E}@{2|tXfg&cog_IqaMhKA@rzMQ`6 z^7k{FG>2_8goYJ&04<>)yR&|zr~gp<_4F!>SMKZRrs+`)QUsezdPCU9ZiuLT>^5-) z>;-WbS^erLtp7~>iI)vtZa?>UwT%qtHum<%WM>1RAUm>^aE`0~GxKLxzx%x3_YccK`x7oJwYyJJ z##!|?vG-8$*MF$LitDRc5B$nqvnuF6?f-2-rFWCBC)~H@JZ51vU|T;6AZ>lffr(>& zUTIZuViTYs_h)%E6hFOFGVkNxIDvaf_nGh@_vha@oR5gi$A^7UQ*n`b1iR`#<30DY zKj+qd;@uUIKnnpTOC{!B3k~Fv#d_328Bdl9k!v#t?Ky=dHa&Y(-_&ZyP$m#%5It?# z)KkW%#L~}Rb>&WN7)}_A(iENqC)*-fyq7RCj3#V?g?0E z%q<87{m6mF6X&@HUoP-0u5h=V;-Z<6Z?K*1(YjRjZz8P)ltb0YqFEZ}!WPz6!f;9f zyI)(0$8-PudeFGahP6>D+aT({W!ZHU=K%d!fz#JF=gk`W8|JW|x3AVm{osLJQ*It` zb9EVM%_;4|p43Cg>qi+Jty$2h!{o_RtQx*du8Z1urHau1_cowIUPWqE^b-+kCf+S+ z_o{S&<_kZXp~8S-tim4EReWV<=7!D$=tm|z8vptBs4IU?Mgd>G{`}`0-P8x$L_ZKG z&Q-8Xavpr~eC>fQFxmt|hUG*rqWMtwIcruQkr>8iwLwQ>*|7S65o{L04Cd|w$YaOB zx$4I-MD_SSVC~rp54FZ>jy}zv)mQvn_469~4wY;9`c52VRY+wm8^DmLw?4bu6kH&i z+W>*qg%y~O4am{eYtuG?>7nA>|BMp0_VWTKw%X<~YC6>DA#!#PHLwhJ>3266X z&DXEomRYOJV+Foi+cnt23PowI z;;J9=aBb1AIXS1xKSkAIX0%{_4bjr#ri!@h>~gU@5%r=rcNWnIl?Jn+jkJyy%@tSu zz=$d}M*Hk|ojcmv&A+--8?Z20IQTa6X$5<{@9c;VhFRo@dc#;5U(}S$OmLO!gnHT| zR7=~<6O*QmJagLGp_yzuoQ<#IW|K?t9aSJeIG9^^{m;vQ(BlIp3sXX#v=)a!*4sh*7SZZx(Gj@f- zl6THkKYYXUr>Rx;S1(z{syBqWk;{wqUY^Q!2Ez&ZNgiv5#kI1_zOvC;=C6JH*Uo9v zL5#^?M$+nOnqbzhY6Xf$;jej`r#@)xLqhhv2_jKt)ej%pP}enT=haBQ57f*?uyJsvm&(KMVF>giO0KqweKLAXUW#=H7O)9Wz<1htq^BQo(%n)!-t_nZ z?a*mwfL^^|mr3D22eb&r|KNUYX#QTMcr5eC!L5!K{6&MWsfcw_2>)82n2BPxi0~2v zEQCeIv?lTO$6r5!<=yo6r{^5bd~WTxen?BRXJ3T=w91!m=!8e=vZxmDh<>_D!|59f zmaT2htO}7V7Yg`#9OJA<5&NbE#z?GsAIPc0qFX}wLkb@hlD$eLwcC!rmJxmDvBnm+ zQX0#P*dvb~<9k{*6s)jd&H@OLnCgCzfaJ0_p6kpEz@^d3cqM zF%dyV{o>78E*Sk|DPhzNZNEDxbxUtQqa%Kar$}JM_tpp_Je}Buydng2gvNPXVh(r zO^KNeY=>_CZ(H}*YoTzWn7$6}t!Bk?(7cuQrIO`##>h{$*A1cThgfB>9Pr($wkH+C zC#-W|@5o*|=G;LE`)yEtA z4?L%2k2@l;G*xH3tP|V`1vMdYshyO)uKL+MBOk6=;P&pBpS9=nn5i=sVC^LWG^9;# z+~H9ff#e!~mm&RK(b*`9dqAzrD_B8iv_d?w*uu(%DSj1i;#RdZ%&zpXG^jtt9Al3+ zi+YABsjm8YJWo!IOI!B!#V@J8M(B>8^q$=bQ^I6>=G6u3L7i~ic*DSgUf0jAzBOXA z1MRuAhaCPa8VVSZF{IZEdicWDQYYz2hj#TU$7_0sM?5NK+o4c#9u)9JqNV<@)xm|^ z9CkXO-8Z`^?*3PiL64BwfLW=f5R!YqN6G@l1k@zGOaK zaUWeWf_3hSn~Itd_{If_Q?f#%cU;u!B#l`%6y*@Mq$_;Wj$I;%U~)J>>!>s9 z60YoXUEPiE#7^z5G`vKcx3@I+`_j{@&CD$V77nr|5lWc9elStbL66@5IWJmYS#b}b zA6+!IWK>@BoON_f60v=g9fk$hL%p~s%e8KMvt0AXw5SAn`v&1I;Vk==s`!$P=?6Xi zG$W7G>$}%>9YT=+y+Awx8Nr%&gC19BY2B0rl=*c40{pjbm>UO}U3Zk%PdwUq=h+WW z4?OWfc|2L7u3+JSg6z%ubw~HNvbn^)WS3(V=PJSd`E9N-wb_}UB9;;fk_Ri_1C|0=eSoWeBvC-;i~I8D?-^~?!(Ial z{`wh4t3Mt;NJ-*lukX93$vw)JC?&Xo#hZ679|uqqs8cc~4R z*=y3%&sDlPDZZ`Gq8kNPy%(%uPqaZlh$*J>8)G>)hv!xW{ir52_)*OvvzJb{=J>I3 zDC(-8?X<$R(V;JYjO}Yx=+AaSK_12~ke+@vl$~Sfk1MtfKWo)n&fZfS^uwf*2CaU7 zE%#tctAc*qlyCWQz3v|B7Gur1!@_!@isx)_FQvZmw|MOh0}EdvUHcE_V;E8E?EJ9! z3|EZ2xFTo|HS|lLqByW>y>SC(VG+GyY$hAp8}XpBmEipKGn(Ee$ve_&gx=ucM-y2; z<*A@T@E-#^&%b~iECqD2-pL-LsH=XuOV2MK)f$~@A8D=sJ#+7a`D#pL&H5meK3Io# z>Eeq1`oU9=;`;Wy-RzT4Si6TcmlOgOV>i8Sm%h*#WVA_|# zHNo`*RfubR6f;2qYer0jB`O(eYMp}bB{ao>IVleW6_x3Vw%JG3nBDROx3*4;mU(!C;Y4mNyGSQ zUsXqUl^=|~YO&_bV9Uw&8Yoo3>R;}=D?Sb0TzF$uIL@w68*Z_mNzZ>YU*oTR)Fh=s zrA{3vboevw5au>O87u~fts0(Dq}U=bZ$(sfWA&j;iGCX9oJmsSyDsO?plD-lw<76>aOsEjAV=KM=*M*F z$N7lrFd+ItUgE@`dRk79cQ)FnpY^353S>2>9~Y({DgSYi_kd$nByl|a<1e$(S5(12Ju+7mANz)Hg zLJl_WfhVXC3l-wNlOPt~qfHTar=l2ck?=*+~ zIvx+4=^IK4EB(~8$^ltf4W|5(rsW`_@x^Tp^G;BlSPwHESC*X3(#$ZVAG((CYOKY^ zw6~oilS|M&Nk5h?sBZ86Cr?#vtr^n1#b&c(RF+P1b3kinw|PHo*8JZe(*+%0x1z9q z)?2b{yf$q?>?&){7qeN}c*UuTemdNufQ((ItDMAN*HuzRj>4#&u2BTSKl~4E2{T+Hl-uk_igB;SFZa{q{ z{d~E=f?4|(wZ3~9Cd7l>0+oF`SgAx0)i0oUdHBrEyK$?#e}vNgEh+gllDLvnfPP$E zddGvmG`Q|XH*aDupdV-#x$)<-q4^g_ActloratfQ!+;+a@{7S?yS%!+&wbb0LB_LK z?W3PaHs;abOVca-T*I34Y9Z^Kh!L?_#CP&piQB$x*QV`nRt*H51sVb6p+WaFzYXa! zzxuR$pI9~AM9$Mc8f=`kQ3 z$fIs{*TOo0O3 zQrU+MWKX-T2(~tCsmNLVGNqBtIhSN>&h|xY1&jxrg@zN#b|@#!5BJh4yxVkDe^VmGF0s47(n-WJ1t@-@aH`)q{?XsVwAKB+)TI(@(Qh68DjCn@) zQIUD<8!D?G_Sd;hJEhc{3Z)dR7P!76O!V^wzw7gJR2}<2x?6K*=CKATh)w-O!X`Th z&i;Pk^4(UAU3qK}Gy?Rq4C}XOI&${XZNsb@`eBMYThD*Bzg63P)*Qn!whd*mKgb@A z-OjfuSQo1=V{af+)Mczo8uGLag`?Zvr{z!F=l|7QHN5zQH;n<7CxK2g-JPQ_TG-r? zSEQI#Y}GLM4n+-xby z&8tQ20p&>A#+E(Pc(a?Xldu=B`` zPw!}L&^99a*`3E5Y+}j?#ka}_A7ddn>EKWnJ_0?} z4-U2I$xWwWi>3lJ+HYo)VMFoT!VVA~*uoxB3H^l7X{G9}2wSrY^9;57csId5 zZ4;|L5?;*P%9@VEbBDc*?m1lleR^OfY4d~oNcd8y)0jVj71vKE@_oVaa+xt_e%${f z?ZdFJV#f2xR_d%^A)^#0fBnqYhf;+@Wqzss0~$n+L(ql)vSz(X|NmU2eynNN!sQp| zZaYAaBQO-SZTf2;ZnBo66~8K&bK3o0xwbY9ZiO#mxWN&N$CHI@-e|-R-$RwYPA}l% z(`O#LI9ds1AB}FB7$w#wB{j~x|MxM%<1W==^5I+NhS^=&qz(d8O%(hi5eC4TMDu(4tHh+8x;>X zO~)%OIvou`W^tA(XYxwL-L;ra=oI6;eA$y4C2CgEZ<>&+C%t@F`IkyfM{CQFX=PaS z0>z`UxTVr_;$l#ze`A)rN~zJ*+Bsww>#CQwyWsB4OiNGAOr?IO#%JP$b2{)lK9Sj1 zE>YFh%4Kq(L!1+`pURqG z{c|3{3X+tYl0G^yE<%#V!Jr!C@4-&sAce;df?Nsmd2kJIIJhSGIMS1T9ZGo%JSshD zyh}=I?7o7VjyV;LlH>s8BRihx2AC3E09OSEq2wyy^faw(X)pA(AU`cHN%g@iAlC+8 zf!q*08#3j07;+tO8f40^5OO_ms7Rl~)92)N5eDJHpskQQ3)uvjGHeE!GOPfu3%*-U zlAOVp!PMF#U`KFTbkf-9R7u(exgz9^g2TYnTM^-D(QP`kmjX38RLX6Q0JX?L@OG3& z-84EiE-fiOO&S{$ml%yMOhze{p+5|!3PgaZTNlHg(w{>!DL%y@N%r7LYCpwM`O8s_ zSDNN*u$3hAbZ*+HxP&-l)&zYtQb?bT)@BT_?It@d(;MC~Y z=<&$s8RDt1`$$LeH$;A)fvGaLP*V(JPHxt?xb(J#Zx{N6L*C84eZ& zbOcj@k&!7G$?1}mh*GIyPn&R#ZpvE}0;X1@ zJy!~*9vFdqE9W3E00ByHu?4T`$1NoZmCfA>CdcXsCXa5^npYqjvK!=dFb&rXqj~T#Jz>i*hD$e%hzM=SWdoLui zM|@ic^gq@3E+Q&}Bm8+rqr+3vqEXxEFp3ti&tPLo{wt|0@Q+sh$VwgFr*ernDeB+2buVk z$e>V6*V>S&!fAn=cR?m6%}R-)5t5399Fvlqo)DKVE$+&PEKBg{@PrhybD9AKC0x~w zk7WvE^4gJVQ8{O~fx>*g$Zl#6UR)NKTrV=NO1JV=^QuHF{L$=x9j_QXd$bTo|u3^&K7bSn9}R{_f6#8D)O&jEkG@*&@3lEnVTnvC#S?kp|UB7 zi5a6M>B1PkGVKJ{MRtq8wDL>@*9MOe`p!b%5=?7vC2&3PedIxW4opk%7I1y=Ji#eE zzEf^51n6MU5{x5RZbh&&_#UP&@p&+1v=dAfP=$V+kcX+WE1Cn`vUoFQPT+hFOs+Nw zOuZG6l9EXKkEoQSjP$s~v@;WVyuSW5SLI5MIcw36wy8X$RbZ;X2I$Gxr=unmzZkMTSX+~k2Uayro;96UEDf%h z6h2zgXP)Z}o*$NPSV~fRpc?HJoM!SKh)PM1PDb@wAc9(6VHU4x4wzOpY)R9`z_X6c z=A*I>Y=S-yOq-E$V4BR~;F{pK1$O|`q-_YM2H1nCCvCvmUP0eHjzvL6L_~neaNj(h z@oF$-5TD26$E8vIC20xt)YXCcyhpsjB>w`Y^xp`5b1)S+82Qrldw~X110R8Ns6c%p zpf%dqWTKC#jJ+z)cmuc!?4r{m!xO`!q9YUIGU5=g&ue|YdN1TXH5*J7$#nBfhMAO* z7@d&{4nscFQG3TyS-$W8kT6GE(C@qnH+uTY;%QVTF7|Ye1%vvk}$! zZ4pmbx{4PUTEt!K4rJp^NI* z(8u@;p#fa&=c$%7v=|mDlH^5khVux$t%aOyB&oN0xS_8d0v%y)qsooU#$AwnASvpx zKu6Rj-V^(>gmt<9LYc?iA8mjdf_24N;9rQTmSvA$W zjlbMm&1_?l*Q)#R_XD-0jYTPOP`zFJWmh%R#bTTQ_wIx=w(4XTv;4JM0-Y}$h03rY zoiPJaFr;$YD18ly?8~VE4b8?naINl;3|eiXAYmK9CHbhD>1vT})cyEtR!dwh#&MXU z?Gdkyw_y`Ws^36gJE2yz;;K7Ik}Hf9Do*aKdbP92*=lAxi^0D(^+_iZ3HhUOdmv%jQJkAuzO9zDw;0^8 zW$@R^NkK>}$FK<@Z_*hbA=DEojOt`pv+-jb-Kj~68sKg=K8Mr`lC369zz%>02hGJ$ zeF2HO5#_p>jcw|qj$FdPO@>7EFR#_&lDgk)F=jS^CqT!$_9IBXkZiPWw`nLz!?n~j z2&*CWFOwd=EXmzeFE5Mn{YE^0txt_Mjj^4BJzNMq&>s>FHm~y>NYnye=Pw~qy|~n@ ziC$-NiwTfK%Tb4IkVHDPr*cy+l~a$onhoiYx~iovKE?wGQCWPX|A0g}7^!~@otwdx z$jUGWp^lpUWrX};fqp>`c5D7pe;SG)4b!YDwBRm@F+fdvLF$Y&71e;&W@A3208OGC zZbK4dfLi|25?5ys$J2$jlBAJk(m_b0Aj#AMqepAr9^QYGA-$tn(xj`{2A%=QR+}F8 zAPwVj80GFRdXG`fmP6ukBt3={uUV25(N>ZMmr45|k>~J!+Jos}tyxXCm(%4!vc}zj zWX+`m9NQ|*g%qKeAm3NLI$PxWY9{{nQulYZ81vn6kwlA!e;SS>*jMY_Y8WE&JopFd zITcb5NENj)`wkM;!`42AAvhZJQA=HYjB61Jgav#CJ#YsS)q#7Ey;+jdA<-Q0w1dDq zfQJ7*qzKi|%g5N)6Gty$WHug$L<7O6O-p#s_%f*&lC`Kfi~?^U4fQ9G$aRqpxwpW9 zB~bNq_c10R#5;_v&Ozc`M3rrXcE)IFsHtlp(Ii9FG0Zjncz%33#z3NOsGxPqHb~S( z+)W=t>I4aHjXX!gf2oe#$#y|PpOGb|&}~R)o>)ukTlj>xQ{@Gob`WSz+i7d!SCA+Z zgC^DK$cr;l;fB$Wg0+!4jnF_XJ8D`>xP1pm7&kAoaU>)vidSPXq&`|4dH0WydTJ6i zGz4|0l6gZnL9(s@Pa#n{?w36}>rJEb=0PG$TW!W%6B5>#w>|Ap_pUrkw7L)ypD?tH zT!BQr#iy333*Q6C>g2{|d9do$*CKCKGy7VM-y#O>!)Vah)I)aEI5Y>#?ixrGr;WAo z2O%L39MKx9`}~V`;!`)MLS( zb`Yrhv}3pYrJ6a&Vl4l*ULwkuJ=7BD#z9Bz(Xy5Is+of=hEnJPX@bcCYRO=WlGjVy z4xGiJOEut!>bH9H)vgAOxg4aH46(?YRj*)+@i}66_fe<&qZaTzQNTpCB-kP!RJ}qh z%Huw2R*1jcT-_gHF-GAiK@HLtL5zdfPzy569BMIS_d~N(zy3b*4z*;c#rPC*J6OUo z;81PgKbTlFJJ6<4>i$rR@c?w>!ZJTlmPyCVX1SA^Im}|5h@B(3s6-80HSa>Y-+j^pSs8 zOCXO3#xy0_xC|lkR^;5qY`g@CazbseZ-wNiW;e#J z1|h1HlQwC*Rj+7l4To`CbUwW4kh(wGVz3z@NrTkG(LRP?go3otdW5i_hwjg^P{2rC zmsb|LS{7;^PUixYBCRa+0YW`AUHJ$-)VD0Oq%8DZS*TSc+4&{-+G+vH?r>SCT$Chr z)3WPb7J9cVbhRwhFq%$YC~a(6XmeTUL0PCnjIc9KK!~~!i}6siaR;RKkhD%R){f=p zMU}N)nQIYt?4-9AV(usEJzc`hHv^DIxx ztQ@13BwLJoptHcDg63ZjA@QRT%>>eJ+SRTRiqY(iPa#o# z_!)yaiAx4*n~bF=6>9@@d|u0#TMd%AD`G7fV3tGF{b?5C66h!o?uy^5Ug>mpn3--d z{5pnCowCz?41H7S#EL?j5DL*kwrS+9qzgfa$L>Oi$5u|4qybv&NQA<)&|xi9n&E4k zL1%cBG#epa#J6Q3%UC_Za)e?vJDYL1C832zAv8h@eN+}|I$lpP6(L@MZxG^kKAF1i zU4({cX&)oxsvX&Vvh;%l7F1&#BDz%KrJ;kBnIb+v>4JRECeCSRXbD}3xtG42swNSNuFp&e>AcuvMd zRZ9ze)y&Bj<3>m{skB3hd{r%huGSQ8kGf+r4OBCySma;T{rEdfEtz65?w`uzarAEQ zX$JvY3(5f7E}v=GG^wRie3V7g)F)H?jbB4SlMrPOGs{kD<}{13$8@ekzhU8D0jWPM z_%`JUBw9u=NgA1DZ#8qeMNU)qPq!F1%-}7>yu^gR45_o03I{#8tC~5(Vw^LRFEjSk zPUYxKHEWK)QfroaV2;0W?ks*#!1NhxR?g2-z32KHuFWR*@|){p=<6hQ5^$$o2B{Y$?Zn0K6C`i-@EjkdL4lgJz~AuB z0#eYS{Srd7_+oyHFe?pJ)m!yfGE_B7^*5YWX_#q7RC|{XKi}d6L!$YCx}h5LA(3zK z1^YN8TCn-)z%xkHCGcC!w{8pd(V?YXg+wjHp&lEW3y@qPp&pIR#utz%PozW7bXuf( zFY-5TW&E53CK!14#p;1Y{>p;I>J!i>i&bytuLRH+3&?7vjs_fc;zfJEgP zv|5*2#{H3>M~sCOtsY+FW4wb41t`%v~pE_^U-R_bRCjXvf)Tyj4o>T#?v7}JN4+VeP6+I=05E3a+lvLR8gYX?T- z$B@c=$Y8r3>D1DGK1NT3$eUn=gZo%Ww1#L~V8b3rzT~UQ@9WhA#s0>?4ZJ;=jX7rH zLP*p{+<`xbM4iJga5UPeuWdM*D{&juC#(I9%b}#)F@orTPa)xoz!V=t!}ll9ys`DKQKkTqR=v$1RZ6VLy_u= zy)$J=&0g>5M7r6e!|xSVx2$iLQv=#UfUH&l^jifeK(S!BjusC;(0;@ekCD^#7!<8Z z=qgRW1zaB31<>zxOy%uHJqK)X3a~@?FhF@85qu0xHpc<_5mUFF6nq*?@n--@J-op= zhorNBEpQR22wVl|M@+e2BLTmEVk6>z1Sr3s0QwPA=|7Wz-^)z--qY?I@c_k>LXy>_ zjn3L=+=oQf`W>Kb9s-p8Bf)=xDgG%y@y`XzFe-;|c`(`A;SUv8fds(@a23duzB;%P z*cnVF^<{ViMKl(QCZxrWn1W65rz+SCCOaRY_Z931rUEU31Hkr>`+@04Ocfporu4yr zhY21jV|tK5Boz23MbRI1#e2><)UHHG6q^F3ZXXY(;(ALt?7n3Bjkpl_7s7;)yBv zxsZve;HzM&5Iwa>4Y~!U3j7SlKk2Rvzo3lo3&pRb#qVXNiu?gRW$;kgJpxmUp9%Zd zF{RTIy@4sItX?`)oo>EL6_GuaP?`SxjY(BS=>I27IordYxGI>sqLxVi8ur!b1>|YMm|hwdqw?qE5EbOWI$`ys+EMA(6~u8~MV zx=<1EGLtGy=wD{ac(~9LQ*b2ykUm^+1S#=*nQKFyDD)E*Uj9o8&Hn$Mto8pZHTwTw z0qTKlw2-FQWH5F8G|}R}a~o37A~07N<_lgR61>c^y5(qF)pxrcU{AN2rNzSPWtP>- z#~gFW=QfF0VzS;U)dE(g zrS%J8c$uu!-p3u?s9(N@M0}03|Al3B#c{_TWOYMW6I1H%g-lHHk6{Ag-Om6l<$i&nUS{8ySN^*;rMlJ*Vk0DUszsfDS+?qw!b z2L8}CY%-WCG(*_E%v8=y=&8Z8MZ8*TcO`8b`;Jh&j;Y{05&t@-4Cf=BdVqncpv7QH zvJ8JHeIb}Cwgyb`Ybla*ju!ErP!Lmwn}y*P!CQrXJD7eiGZjEj#Srfk@x)Z%VKC)) z1WW}V6?{zaaqv=2h$<_OQ9#G zebqJc0s377Qvuh(l)(+b6!(Af^sh^O6hH+x1Udo{0C_vk`?TNdm@4@1>EFMnfAE8U zPyhZs{ljWND+v8w!?alZd-{h?@DrUwOsjrJA-|6QJ^kZ{!GBNx{yqJ}RQW$~UP;+% z`Tl$QhmQF7^sgnY$@sm@bnN=~^zU`2ebiH=r(@W^r+@#SPycEOr~8l7zxS7XG;j{9 z8zR5WJO{{?Sq4HfW5+_|A@XA8G!(RijUigf&JZnQ4MIW7Stij6c8O>uYcUK|$R-o5 zVpoWYm|K`Ugar>lwhP0M?P_+D6n4Q-bQuoCT2?R|ihZPbNQ(6=U<4GSLZDbX0*a07 z0Tl9ktj9>uCRRkWnUxT2Vg15ETiHgUZA^{;ZD%1wJJ=4Qoy-^sdY^@Z*sL&QdomK) z?q*e^pzs_HMMe}9d)YBk+$KenXejoxG0{*g9s$J{q&UbL#6Zz~BoxzQpg7Dfk>W8a zJYu0Z!Y0Q;u^}9aTckL~+(tnW90A3`QBa&TlfsY)#W~h55sL9KQ0ykf1tup!(L5H4=p-mE zu^puNiWD`Hq44ScB~m;lg-0qB*V*J$C^jTQaf=i;nOhnZ!AVdoOoQTkc9Rr# z$xw7jhvF71NQYt{DISvICl-(a#i$f0)@DF)hdm%gttiU-pZDz+8B1>W@Tlzqe#BKw{MPINqPO>I3ArzyO!BH6CyhYPg(mbsLpLDen<=h~}mYmt24;I;@i>sY@P@{r2<|9fn`1o!RO z4kmZ5d>#+q;J!}oZE!u!ifvwvc5TCxHTGoUsFh;3LiV%{#vz%yOYPOLu~qk`!TNt$ zzgDhzRJ;La7yQ3}WyLq=`3wFdfA|Ia9oq8H*(d#USsO-iq|)yolUPC#_|aWtN=H{Z zLr8(&Z$d|xVOwe%NqQi3bmg-b&o4*%T_|ZA{+1|&ZbRdrb~|ep3ZYE?go4VW%Dp2B zrrXnGR{@wKbbkpuy7}<2(3J=~x>`kdW9j!q)8YCr-C$oXl+-);NB`?oIwUGX_q<70 z1xORRQYa`1J)A(-`>91QgdIKLK=T7b#~>$o0^DQ0?G;nb4Ftah!VxiMtg8$AQW%`+5)YBk$?x#1ZW5}1^NRWfIfg3z!P%x zsG9UPfK@Sv{!17=Ye`Qv(nDbM0I|R*AP$HJ z5`doQHJTN@fxbXLpbtRzmns64fXV<(k}AMZ*el>tFrB_V1O9TtAG*=^CqTCke*mrn z(GqJ|O&OjOfm%iaD}gz{TmTP?O7npN01LN-1zUO-SO_czusTc2fSSn10dNG!iD_cc zP3P&r3}7ZO8=%=qvymRcnT>RG&prXjpoat!5l91Kfw4dm5D6p$qkwTh7LWpr2Sx)? zKspcyWCCM=2|zTE3d933zzBfWr#|2ypf3;x37TzF6;&Y^vJ^o=r;nib~FZ>08N2rKntJi4>Rkj109tdBfziMKU=cE8Ks|)(0(F4uKsA7F z<<|xr0XnyE0BQg=fvUidNcR(P8@L1f4A32(LVy;|#{eys4`j?wTAA+x9|FgKSYQ+o z57+}$fl9!AB>olH3Ty{<06PJC1V>?2YbcF!3J{zJ(DNnzffIu-S6!;7n0`vm9 z0)apP&<*GgbOyQrtC4OGcn7c@I0C#690&FS`+!}*Zr}iL7&u67cL;$GfD^!nz)4^y zupc-I90O`2bGk=*5}+G?$AQDZR$v>j23QNM1L&UX1YjI69-y_D9-XKUb_N;)O@OAr zE?WG)MMqr+ZUWx{^eES9;A7wu;5_gt0hRa+_#F5OxB{$#z6dA=Rs(B*wLlh|T@y=g z4Fny5S^zzMSsO3`b%2{l{vGfa@E9lo{s5|=t`&ik2%iEz0_c&3FM(62Go8`WP0%C2 zd%z~(9bg_X7YGKL11-o?e?mc5k-;~>0fY|%n}IPvDv%B=1{MK%zFab$lEI18Ks zP63pb@}Mb4HXi|+&KCiF4w8a0rN=5iMD1yg(ww9Oq;CP8p21A$zGM%ZD$Y4nU&@c(@ywzfVSQD>Cw_V2>cBE ziTcrkeGj0$`)|MlfL3h*8l7KwuB#fg6fv$YZZ7Wb z(li!^r1By*u7%=`D_EO|_Oa_k$6583io5AN5@TPTTi7JGb*t&o5lHOe;^pFw<2(y* ziNxV-0?0(Is0KUNyo}o^Ux)q$JGXWy-%Z$=$kDLf%dNZRgR5u1KRnfH@IA|Eg^YN9 zCfb8&1$G)bvSrcf2V{qQ+LaY3?EY(h>-H{brKH)Qb(Py9`$Cp80<@sMt? zYq0dtUs8AT){3e3k9+3vtZ0Dr*Vv7plYY4Ry(jY&*&Pi+58^=I$`ys^@4xely%;rd zW_)8LZRds&a+7Ma5is!3Uz}&YdwJ(?&WKGh-r_MD76#Y zLV4@&;On?5@r={s#D`k7%w#Zv-GqUh!czLcPJee_g`iJzKVEgM1J%LB4HcE9So=$I4jWKLFoC5_mZ&kGplMb?L3h56w5|Zll~UOO@I7V?E1y8 zr(w_@)khufu^ljwCH4u~=`TF3blUS${h{UFSL6=pYd6@nW-rKqzaka?;d4@X=CL*P zz9PH2n90SZFy`J4-m1S_)g<+8cwyC^$|#AP2RZCwlgU7TDeK0ABY$2!C1$KGa&T`? zSzKlp+R-rLtkfB9+Ii2@YnKicrrFAFsA_wf1?BlG--2vSb(1O{54XlNV^un!cKQoq zyC<&+e4b!?&1w+N`njW-IqaAR?DRL!?m1SxvqQC8Ypr&NSt0B^^jFu89N53sy@3b& zSPk@-+%~YSvg-Q49p71FOzamVHR&&_op3MWw@Ont7F*lbg*9l8I)pMGkV$_F?vptI zjV67zB*SVqo29_OLw}9#uff+AzSVQLqt!rv;cgvM$(&p6RW4g&F0jLt^oAXOA=JU| z7hY~Xzw$n-f&Nb4(d+#8O_^}F&>B;bRYcdwL9B^~;_mV0X1=*=c;Ed)=kngidPQUL z4}&E}^UcG3mv!!d1yW2f{SBV^=bz*^`^_Z?E=TJg=B6Fn1h>G|=94h@&|eXnez5A1 zL7!ZE4m&L0qnv*#+rQQdK!33okFV@D4y6W#&ZF6+P)wPapaWJ5z9aC=r z3x|O`f=wVwWDD_M_rwy%zB4PelRV)!J-O%OLvOoBI0qwOpQ266_I3< zNQaqBN+Ah4$AaFd)Jm&qi*^WOQ421ydfrMOyp5P8dn+-x)zQiinV)6>gO!?|m~Q&} zf`4ASs>9~1{*JV0w$nQ0h7&)~o&0LkvN;LAW3|*{?lV6h#Yri3Vn=dE;x%N_l2tV||r&CjG6)dB4ZH9cwYIk{CoB4#L?r zB!G7j>80rWYV)YHTWkc{TNbaiTkl(T$`2#gk=>*o8O)v%#j~nIKoeP8e>jBxPUN8d zD|glYVjww$@R(v&cPQ-kv21^gV^6jN<`}Y1h(savSV3n>axOwff5v=jBLB697r` z*qH7}DyWe4>j+8oGp`I=*-@#jJZ-`bbX3~uUK+w4&`3nH21LSXgi@sDl1YE%^5sRp z&YgR*D(w|$VxVYub~pg7dnGG&IRKRxs!v!*C#!*IktouEUG0RDUD!i15pkT9(&lV< zXE@iZoyo_Aeb`xPFN^-Y$PNxfCcM?WXb~ea?8xMJM`m3uy z^z6~6p8TV>1L6b8{Zsywjp%8uUm@G^HtHwJdbQ23O2S0=o~WacUTU6PjRo{T{%u(( zk!Vs^mJi9Kzb1Qse9J1i(v+LlroY*&R@2Q*$YS;Zg}ufKzQyLaOwwJ&}ZW3@ZOEM$PU zmTNC9FFyIW(bCjTv_y&-Ae>!zkMA2WUB`LM}DG>-1D>}Eb|?t)Hdm_i1%wf?&Rvy zgP)?p!oNj#@DwHmw>Pj)%grm=IjA)I!XVh;42KRiVhd#5?TABzc8IUZJ_}Il>PL&a zEOantgfQU80aHsHdL5DTPABKLk5z9(r8+Q__Z7?~7>)n0`-ljJq<^b0n-!|mVAtPP4DwO-A{Yac$DBf7 z^)c%m0!d3lG?|p5t-Mr^;EuTBing}e7`GP+May65 zRoRvOf{locILS`AJw32gK!Z5|jZZ&R&RYYsq^JvA<7SRLQ`e8~ej`mf; zaGs>Ua{rL&(_pW=jh-PJ;d=Vp`tLQl5_3OnPzhqlC2&qLgM}d}HuBd&YxkKcxS1>G`iuKGxD8KU^o(jPWE>Ntm)5=G%s&aQ%(`_g*Zj{pVVLFU?%LaG*VL zfm(Mdh&q^=W~M!c@JF81q`$tu)x_fBx)tR*(EY= z&hCOt-mt5JswT~xD*e!N*CN=p=NH9#Fz0Z^sm(|jRENQ5(^iITxa<2HWz^249ZtNa zBBmx{T8%08xw6$^hl8BPqQjL2rqhU^>u7BFoRmg=&%Wy*FM*~lG?!Ts^29?r2SGt4 z(Bjs3?o%7L1%YEGk9Uy${on&;7q^=IaGM7)(=!6Yx1U>o&_Ol`O*1fE+ln2R8))>Z z6sxsMJCSo^jUv!x-fRd_Ae%#^b;$*JAbt^c@khr+~)wS|p*|pus>#M8EID_L)eaJ6o~T> zq6X|1nd_fD$d34V_1+_gTEU!VktZ(4vXp$-xv<_bC|>`5z_;HXS+S|zVK2I9fU8Sx z?WA&SG7RKeY#G_<-yb-3t6)QQ$8LXF?R?n<7c%2({Tm2&RsCD-lixjSHCW2}!2l2LC6Jx|5ryN@{ccU~C>^bKmsueU zJoK+Fw5-IrpvvpAbu8J_}D!9Lm8%a6^ZM#iKI%rymSoKkbsV z{oHmK3kToE3SfZwvx)5VSI_rpd%W1`;}bM-I^dcfCXSYAnO(OyzFgmn8an8j@6kZm zxna>oiTzod1Z1Rtdt#`({A$;~3TZD)mvF2Z<+BN}L%IDXD>ZGQ@5Z)KLj7YCn^W9& zCEkx*iG-qAZtVL6WiqBNjvG$$<-NG3Y$KcW?-3NV9XhY+Za*&v`DZ+dh8yF#c5Fow z3?10sBrFj5tm+tK|K=H-#+;MUt;15Z|DTw9(4l6vu>2)cY-L>Xbf>GH<5*HMa?rmX z(QoPf`yE$ZdTuq?$%`&>^NVJJN+{tojz#1x^c0z!fMx?<&&Ko+c+9WdToGWu|Q^y zfi#KT#Bkw+V@ImioEoXPE~tNUW9FIQXN?;9{sG?*rxp6gI(iI`y*+icEgjKu&7UTK zmP5^Y*bn>{oBzCjV_!B&GS~}L!=!)2O~+v@dG^u<;l?i=9W>I{Jq)MB->u(Q@b z2y*w%oR$t@Q*qA+W2c$zVnJDoQwQzK9_U8T7DI-e_^@tgEgx69(V&0(qhGZ)nN=<( z_|qEe;nEJ<$A+`n$}H4h|5`|rY`i@4-N;o|3;lZ{ZO7i2Z@k=Ki!~;EHoJ$kb@VTg zjB8YEe|_%6M5{&KY*ruj@33w*Uo7^{8tOB!N@#DZh5l8O=24cSuAharv&M+FchEmK zGXK%qRR`+YOhK#M`Ry|O10{F*&;8J`)+M@rgq|fo+c%FLnxM2(;`7*VC|`_dozL^Q zpN&e}`r=8IO>x@ZPFrlh&13!(QBnPyCFWyu_AKgr^8&IIClmTtPAu#0JZo1knQnTK zQ=@YO^VwXat@8#8bn-X(>?m32Ur52e@uR?^v*WZpv^}^!(3P^4i^|9X_H8yQqkq+6 z!oApYv#Q-~=peVmJtO+17VyK0OO;<{UjE?+MjJ&pt)kflto0-m^>?r1TS**yT;C~R zBVo~DG4h~I>4S%@-CNr+qVh@sta)9SWgt=ts5-CFL?1?(Bs>~ddz`jk6jSbq7{7n^8x$8AA8 zv{ArZbC7%W1>BDo!w@|z=u&h-qjJrk~1Y;}{;A|ZYq7qCf`HW(JTn4kO3 zk^S~pzxjSTSzwIt8zW*`1fJ+v_2dSvz3sG^FqNIn!3~4+%sp3IV3*}8PIcZqqc^DR zVy;rlL;p_A57+vA6!%MAe$J+ywCG=`8T8q`##t%$AsA?JF`XaB>!GLguSS(7%G*ab z4E$PiHEl1V**GcBMzO@na9+LxP`nqiRS-@3S8gr^zjv|6pM|%uSkjGMH1c(;q)GpD zO5^;i`Mn=}+ZX1z_vq5jUE=F!9sOG?Q3*}P{y4V$6|2F{MQrpG^r`-N7&ptBCKIPt zt*yB&7R+|ir;D`5c5~wgZ(sAh{Zu-s#Ck)1aD5T`fYR#UviUCfZdh~shXd&LD%N&5 zn*Oz$cUzxz4eu6g{SD>$(cR8$nBQ{6<@OG%veWQc<3MQ+0l7H$=*&~ z_gM4LzxC5;g~fmFj*)cr*MpWkde|Pv1P4|$RdKGfgmF(>SukizWYU#j;jK6~t+beZ zgA#C9sX7NIK(!V#`)Lq);&R$aEkC#@wU)5qh*Hp7{EHi4H9^cR6R@kWP@wHmqo;R)wxh+4I*YSAv(ZUNg|u`j?EZb~cs1<=+IbO|lbouTy7FEHreGzv9+v}e4>^Pf>3Z7tAH{i{b;W;K28A3Dp37;%m-I!f=L zJ~MGJdu_cn@8{Oy2yW89k@R5W?SA$HigzPd+CO8B=Lh_t;w^ z0mnP~$`gC*9n>MTh(EQnu+oqIE3ef4Rx3$6%D*`#Z8aN)w5Hjs`DS(cfX7|r9{!z> zmX4TkFeL`@DhD~l-IJHww8^Y1K-j=26d$M zvafvx)uJZRehW=nvzEO(2VJslEjtb7R~sBymAQ(cj{fZ_zdw%d_}JcK6jI?j2rVrs z>zM0Y%!Xs@SkJjgy?H&G1g@ihZ0cO4-k%TM{c8(F#u}yVI%jWSXOW_g{u!z{QSa_J z^!1tRWT9P5kQ_EHnuiK(V6=OrSnHbmNc J$7WgM{{_QC99aMW diff --git a/package.json b/package.json index bb2650a..95fa7a8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@astrojs/alpinejs": "^0.4.9", "@astrojs/check": "^0.9.6", "@astrojs/db": "^0.18.3", + "@astrojs/node": "^9.5.2", "@astrojs/partytown": "^2.1.4", "@astrojs/sitemap": "^3.6.0", "@astrojs/ts-plugin": "^1.10.6", diff --git a/src/components/ContactForm.astro b/src/components/ContactForm.astro index 580ad44..2cdc617 100644 --- a/src/components/ContactForm.astro +++ b/src/components/ContactForm.astro @@ -1,51 +1,30 @@ --- -import { POST} from "@pages/endpoints/contact"; -import type { ContactFormErrors, ContactFormState, ContactFormResult } from "../types/ContactForm"; -type Props = Record; - -async function handleFormRequest(): Promise { - const errors: ContactFormErrors = { - name: "", - phone: "", - msg: "", - code: "", - captcha: "", - form: "", - }; - let success = false; - let action = "send_otp"; +import { POST, generateInitialState } from "@pages/endpoints/contact"; +import * as ContactFormTypes from "../types/ContactForm"; +async function handlePost(): Promise { try { - let response: ContactFormResult<{ nextAction: string }> = + let response = await (await POST(Astro))?.json(); if (!response) { - errors.form = "Invalid response."; - return { errors, success, action }; - } - - if (!response.success) { - errors.form = response.message || "An unexpected error occurred."; - return { errors, success, action }; - } - - action = response.data.nextAction; + return generateInitialState("Invalid response."); + } - return { errors, success, action }; + return response; } catch (error) { + let message = "An unexpected error occurred."; if (error instanceof Error) { - errors.form = "An unexpected error occurred: " + error.message; - } else { - errors.form = "An unexpected error occurred."; + message = "An unexpected error occurred: " + error.message; } - return { errors, success, action }; + return generateInitialState(message); } } -Astro.session?.set('init', true); // Make sure session cookie is set early, else error (better fix: disable html streaming maybe?) +// CANNOT USE SESSION INSIDE AN ASTRO COMPONENT! MUST REVALIDATE FORM FIELDS OR CONVERT TO REGULAR PAGE (preferable as there will never be more than one contact form) -const { errors, success, action } = (Astro.request.method === "POST")? await handleFormRequest() : { errors: {}, success: false, action: "send_otp" }; +const state = (Astro.request.method === "POST")? await handlePost() : generateInitialState(); --- + + - Contact + Home - +

Contact

+ {state.state !== "complete" &&
+
+

Use the below form to shoot me a quick text!

+ {state.error &&

{state.error}

} +
+
+ + + +
+ +
+ +
+ + +
||

Your message has been sent successfully!

}
diff --git a/src/pages/endpoints/contact.ts b/src/pages/endpoints/contact.ts index 0f5abcb..87a5545 100644 --- a/src/pages/endpoints/contact.ts +++ b/src/pages/endpoints/contact.ts @@ -1,4 +1,4 @@ -import type { APIContext, APIRoute, AstroGlobal } from "astro"; +import type { APIContext, APIRoute, AstroSession } from "astro"; import SmsClient from "@lib/SmsGatewayClient.ts"; import Otp, { verifyOtp } from "@lib/Otp.ts"; import CapServer from "@lib/CapAdapter"; @@ -7,7 +7,6 @@ import { OTP_SUPER_SECRET_SALT, ANDROID_SMS_GATEWAY_RECIPIENT_PHONE, } from "astro:env/server"; -import type { defaultSettings } from "astro/runtime/client/dev-toolbar/settings.js"; export const prerender = false; const OTP_SALT = OTP_SUPER_SECRET_SALT; @@ -15,9 +14,16 @@ if (!OTP_SALT) { throw new Error("OTP secret salt configuration is missing."); } -async function sendOtp({ - phone, -}: ContactFormOtpPayload): Promise { +async function sendOtp( + phone: string | undefined, +): Promise { + if (!phone) { + return { + success: false, + error: "Phone number is required.", + }; + } + const otp = Otp.generateOtp(phone, OTP_SALT); const stepSeconds = Otp.getOtpStep(); const stepMinutes = Math.floor(stepSeconds / 60); @@ -37,24 +43,31 @@ async function sendOtp({ } else { return { success: false, - errors: { form: "Verification code failed to send." }, + error: "Verification code failed to send.", }; } } -async function sendMsg({ - name, - phone, - code, - msg, -}: ContactFormMsgPayload): Promise { +async function sendMsg( + name: string | undefined, + phone: string | undefined, + otp: string | undefined, + msg: string | undefined, +): Promise { + if (!name || !phone || !otp || !msg) { + return { + success: false, + error: "SendMsg: Missing required fields", + }; + } + const message = `Web message from ${name} ( ${phone} ):\n\n"${msg}"`; - const isVerified = verifyOtp(phone, OTP_SALT, code); + const isVerified = verifyOtp(phone, OTP_SALT, otp); if (!isVerified) { return { success: false, - errors: { code: "Invalid or expired verification code." }, + error: "Invalid or expired verification code.", }; } @@ -73,7 +86,7 @@ async function sendMsg({ return { success: false, - errors: { form: "Message failed to send." }, + error: "Message failed to send.", }; } @@ -88,9 +101,9 @@ export const ALL: APIRoute = () => { ); }; -function validateFields( +async function validateFields( unsafe: ContactForm.Fields, -): ContactForm.Fields { +): Promise> { const fields: Partial> = {}; const printableAsciiRegex = /^[\x20-\x7E\n\r]*$/; const sixDigitsOnlyRegex = /^[0-9]{6}$/; @@ -166,13 +179,21 @@ function validateFields( } break; } - case "code": { + case "otp": { if (!sixDigitsOnlyRegex.test(value)) { error = "OTP code invalid."; break; } break; } + case "captcha": { + const capValidation = await CapServer.validateToken(value); + if (!capValidation.success) { + error = "Invalid captcha token."; + break; + } + break; + } } if (error) { @@ -181,31 +202,32 @@ function validateFields( fields[field] = { hasError: false, value }; } } - return fields as ContactForm.Fields; } -const isValidState = (value: unknown): value is ContactForm.State => { - if (typeof value !== "object" || value === null) { - return false; - } - - const candidate = value as Partial; +export function generateInitialState(error?: string): ContactForm.State { return ( - typeof candidate.state === "string" && - (typeof candidate.fields === "object" || - typeof candidate.fields === "undefined") && - (typeof candidate.error === "string" || - typeof candidate.error === "undefined") && - typeof candidate.hasError === "boolean" - ); -}; + !error + ? { + state: "initial", + fields: {}, + hasError: false, + } + : { + state: "initial", + fields: {}, + error, + hasError: true, + } + ) as ContactForm.State; +} -export const POST: APIRoute = async (Astro) => { - const respondWithState = (state: ContactForm.State) => - new Response(JSON.stringify(state), { - status: state.hasError ? 400 : 200, - }); +const respondWithState = (state: ContactForm.State) => + new Response(JSON.stringify(state), { + status: state.hasError ? 400 : 200, + }); + +export const POST: APIRoute = async (Astro: APIContext) => { try { const initialState = await processRequestIntoState(Astro); if (initialState.hasError) { @@ -217,24 +239,15 @@ export const POST: APIRoute = async (Astro) => { return respondWithState(validatedState); } - const finalState = await runStateAction(validatedState); + const finalState = await runStateAction(validatedState, Astro); return respondWithState(finalState); - } catch (caught) { - if (isValidState(caught)) { - return respondWithState(caught); - } - + } catch (error) { const message = - caught instanceof Error - ? caught.message - : String(caught ?? "Unexpected error"); + error instanceof Error + ? "Unexpected POST error: " + error.message + : "Unexpected POST error."; - return respondWithState({ - state: "initial", - fields: {}, - hasError: true, - error: message, - }); + return respondWithState(generateInitialState(message)); } }; @@ -266,38 +279,43 @@ export async function processRequestIntoState( throw "Data is undefined."; } - const action = await data.get("action")?.toString(); + const action = await data.get("action"); if (!action) { throw "Invalid action"; } - fields.name = { - hasError: false, - value: - action === "send_msg" - ? session.get("name")?.toString() - : await data.get("name")?.toString(), - }; - fields.phone = { - hasError: false, - value: - action === "send_msg" - ? session.get("phone")?.toString() - : await data.get("phone")?.toString(), - }; - fields.msg = { - hasError: false, - value: - action === "send_msg" - ? session.get("msg")?.toString() - : await data.get("msg")?.toString(), - }; - fields.captcha = { - hasError: false, - value: data.get("cap-token")?.toString(), - }; - fields.code = { hasError: false, value: data.get("code")?.toString() }; + //TODO: session.get returns undefined always + if (action == "send_otp" || action == "send_msg") { + fields.name = { + hasError: false, + value: await (action === "send_msg" + ? session.get("name") + : data.get("name")), + }; + fields.phone = { + hasError: false, + value: await (action === "send_msg" + ? session.get("phone") + : data.get("phone")), + }; + fields.msg = { + hasError: false, + value: await (action === "send_msg" + ? session.get("msg") + : data.get("msg")), + }; + fields.captcha = { + hasError: false, + value: await data.get("cap-token"), + }; + if (action === "send_msg") { + fields.otp = { + hasError: false, + value: await data.get("otp"), + }; + } + } return { state: action, @@ -307,24 +325,127 @@ export async function processRequestIntoState( } catch (error) { return { state: "initial", - fields, + fields: {}, hasError: true, - error: error instanceof Error ? error.message : "Unknown error.", + error: + error instanceof Error + ? "Unexpected processRequest error: " + error.message + : "Unexpected processRequest error.", }; } } +function nextState(state: ContactForm.State): ContactForm.State { + if (state.hasError) { + return state; + } + + let next = { + state: "initial", + fields: {}, + hasError: false, + }; + switch (state.state) { + case "send_otp": + next.state = "otp_sent"; + break; + case "send_msg": + next.state = "complete"; + break; + } + return next as ContactForm.State; +} + +function prevState(state: ContactForm.State): ContactForm.State { + let next = { + state: "initial", + fields: {}, + hasError: state.hasError, + }; + switch (state.state) { + case "send_otp": + next.state = "initial"; + break; + case "send_msg": + next.state = "otp_sent"; + break; + } + return next as ContactForm.State; +} + export async function validateState( state: ContactForm.State, ): Promise { - state.fields = validateFields(state.fields); - // if state.fields has any errors, set hasError on state too and set a message - return state; + try { + state.fields = await validateFields(state.fields); + // if state.fields has any errors, set hasError on state too and set a message + return state; + } catch (error) { + return { + state: "initial", + fields: {}, + hasError: true, + error: + error instanceof Error + ? "Unexpected validateState error: " + error.message + : "Unexpected validateState error.", + }; + } } export async function runStateAction( state: ContactForm.State, + Astro: APIContext, ): Promise { - //Todo - return state; + const { session } = Astro; + + try { + if (state.state === "send_otp" || state.state === "send_msg") { + const name = state.fields.name.value; + const phone = state.fields.phone.value; + const msg = state.fields.msg.value; + const otp = + state.state === "send_msg" ? state.fields.otp.value : undefined; + + let result; + switch (state.state) { + case "send_otp": + result = await sendOtp(phone); + if (result.success) { + session?.set("name", name); + session?.set("phone", phone); + session?.set("msg", msg); + } + break; + case "send_msg": + result = await sendMsg(name, phone, msg, otp); + if (result.success) { + session?.delete("name"); + session?.delete("phone"); + session?.delete("msg"); + } + break; + } + if (!result.success) { + state.hasError = true; + state.error = result.error; + state = prevState(state); + } else { + state = nextState(state); + } + } else { + return generateInitialState("Invalid action."); + } + return state; + } catch (error) { + return { + state: "initial", + fields: {}, + hasError: true, + error: + error instanceof Error + ? "Unexpected runAction error: " + error.message + : "Unexpected runAction error.", + }; + } } diff --git a/src/types/ContactForm.ts b/src/types/ContactForm.ts index ff1dafc..da61c40 100644 --- a/src/types/ContactForm.ts +++ b/src/types/ContactForm.ts @@ -1,4 +1,4 @@ -export type FieldKey = "name" | "phone" | "msg" | "code" | "captcha" | "form"; +export type FieldKey = "name" | "phone" | "msg" | "otp" | "captcha" | "form"; export type FieldValue = { hasError: boolean; value?: string; @@ -24,11 +24,11 @@ export type SendOtpState = { export type OtpSentState = { state: "otp_sent"; -} & BaseState<"name" | "phone" | "msg" | "captcha">; +} & BaseState; export type SendMsgState = { state: "send_msg"; -} & BaseState<"name" | "phone" | "msg" | "code" | "captcha">; +} & BaseState<"name" | "phone" | "msg" | "otp" | "captcha">; export type CompleteState = { state: "complete"; @@ -40,3 +40,13 @@ export type State = | OtpSentState | SendMsgState | CompleteState; + +export type SMSResultSuccess = { + success: true; + expiresInSeconds?: number; +}; +export type SMSResultFailure = { + success: false; + error: string; +}; +export type SendSMSResult = SMSResultSuccess | SMSResultFailure;