From b5f9f52ffee7917f81f42013fd96802ad7b9e016 Mon Sep 17 00:00:00 2001 From: Erol Date: Sun, 12 Apr 2026 21:53:54 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=B8r=20vi=20begynner=20p=C3=A5=20SEO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/icons/cropped-siteicon-1.png | Bin 0 -> 27708 bytes frontend/src/app/facilityData.ts | 1 + .../golfbaner/[slug]/FacilityDetailView.tsx | 16 +- frontend/src/app/layout.tsx | 5 + frontend/src/app/vtg/VtgExplorer.tsx | 661 ++++++++++++++++++ frontend/src/app/vtg/page.tsx | 32 + frontend/src/components/Header.tsx | 1 + 7 files changed, 712 insertions(+), 4 deletions(-) create mode 100755 frontend/public/icons/cropped-siteicon-1.png create mode 100755 frontend/src/app/vtg/VtgExplorer.tsx create mode 100755 frontend/src/app/vtg/page.tsx diff --git a/frontend/public/icons/cropped-siteicon-1.png b/frontend/public/icons/cropped-siteicon-1.png new file mode 100755 index 0000000000000000000000000000000000000000..c3babac6deb742779912aaba5c342eeb7221b9e2 GIT binary patch literal 27708 zcmXt9Wn7eBu-;|q7Nn(9>F$s&0fX-DMy1)MLpr4-rMr8P?vh4WKxt%=?)(0K_ufzX z;oWm)=1e}%5T&J|gojOu4FZAiRFvi4fj}t0e^Ee~=)gbc-al_aAUcqW{Ob?i*+;*E zoQ$u0c5kkBBjpI6M^wyX8ojPa^{x@Dc2Ex1`KT@Yv7tKav^7rnBWJYkWc-`ipC6Oe zg(py#;lt>#XXN^fROI@?ct)&mez7ESF}Jx4ek59a`+m_~vb07itwPK;)pRzDKg~rlbXw2gLeLk5Ww@RWkjpok;`69=c8Mdl`In!KWab zokvmO%>LZxMj$RaW6+Z=Lc)#4v+ibt|JVKJ)G1?6(qxF}Y~?{L$-_+;;d*8?K3%BB zAIa6z@)6t_#yzwPvn#!Wv@Y~I8a;v)T-;z`!Ke3!&}s(DpjdydC=m!H#$YeEVF+IWn#xxJ}wyVXNu`Sb_CNO}Q*uF&zL|D~SYMK)0+pU4UKu272R`|Q zI~`k(r<}!;1$~9&w(s85<%L96PRq=tS#s%RlV-bA6CpJt`4R3O@s=zf?JD;~LZ=LiPB}{oRg&N9>^%Be&Qv z;pi(sU{ZLK0|5`_QR9v1W)+;JC>FjV8{+7RZe=Q;T_*|=<2(&Q8Ny|4@U^VpBbQ@K z288q`rYR3zQyb(m6GoopaSCkw#elBMLXMpH!?Ai%z1MbH`+J50(Razb+&cHJWt9z> zK=hsX1lk?Loy4tZ-mDX6mk%Q0u&LdA%0iwAlJ;B4>&<`byOXUD%IU&);x|u1#Z4%d&Y7E~~7-1j=V@(;4`r z+W;mwgnb4HA`CJK9xNcZoK`&id1Q&S1NRg>HW^6*#KzciCD9#5du%saB-VdRmQ}Bu zgT*;qo=11x$8EkG=Z6bFQ{vrq3w(=V8b^$lUnq}4C>HEE?_vh>BlH&ET72DsgU|ka z(lSFX`~))=yelx9@k*?mP)8OJ!P)0z2iN)`vI6GZ9H{?K9Ez8>G)M3Ty&rA-J>rBh zZaF*Y45#iD;fg-+@v--YHpQjE(JweR<2wEk=z@gWP&uarnml0z?Lo`6Cb4Y_5+H(2 ze*%2cnVFWxh}Da6wkEg+qq)DXs}{3iX&c@Uc@mO~I#_7N)A--37$V)Q8Qu7ij&S^< zSMFZi9LD(BO+F^q5s^wCXanl@@?`D8fY0pz_%-Kir`MpnU<8u1qT5TJA_&HQkl}J; zciWy}>fUNt@s{zV=m8?8{4=ob8t;H*4$KEXvK`@!O^NbEQ9vYJuG_0CrAQcKWSAD|hu%1?lBLO{xLP-Mk{uxjHt=%{+=17dVopPFEs!QR=`VDk{l>PPsOYBX~PP==B{1cR2 zZC7~)!Xy$b8RfVLbpC$O>__F#D8eX`3PU76I+q%hHNmFfIEI{gSh@w~TV)Nv<0vNV z{nPJyJD`%`^nw+)a%_b$gYj6GmXb=7KGe}tDD>1o&@laOQf(O*n&uq#gF5F`7i?pj z+e_dG)OO=NdiL|qD_JhcDF64iTGiShA@B)GAY_S}(@(C0R~<{5;9k5?3AN;dtGwT) zRw-awbN{=`FV$nhj2nW&U%Aun+A|ImK4j#c zPYXh@onNJy_5IUYaNQ?GfK_^k37!ad?=ylN@S(Jq|F5&t?nH zp(p}I7hy`RpJ_~RemJw)XaeNDL*Or47=>K*jZ1V5K1E}@j6a?$oR*$jvJ+}!H3|$%%DfdX#+FJ#N`AP z^rQS%%l*+ygnrRoV(v67y}(CHuTdHFqo7si7;OW64X-_Or;njg1OE${o3e-PniYZT zMtVqOG04N2@zrnj8=4JX!#kl02zS#FZrhb{>Skw*O%l(QlCeyDR?ij9rj!}PhOq!; z#lpWyYkrzUI5RC1ObLI0gXgKzvEx`%KKOI@1-I$!LjnoT-TN~2{+TlHTk>+;!9?O{fyfMrW6&qNvSoIO_@NO zZ`yQ>F=$YvtOdwvGZqE0zCcP~+u=RQqJydPwj@eTq?{W-#WJQzSh8-_sikf}>#CO} z42+(FT*2^5*F_Ay1n8aJr3qYzn+_BaK`24wjdgq8vs?D#6bJ>xNkSZ0N!vA>Tco!6 zk3rK$?fV^P(x->D1DRBto?xUU@lwzipJAVkvOO}wb!9-B67T=YjbY+nlyLjp3w)7p z;z2>rPp_IagEg>)1^7q@blLlx9BdkBd?KcWj=n*v<(#s`bIFfl;EZsHNRSDJsm9;m z%4{aUQLNr}JdATfeZL$u&SHvnBw~D?dH=X}4b@DcU&sQ2E>Y2cp#@`W40UKyJ_UJWUh42Z5qo6jtLIvtkdWv+TMfInl}oFC&FIN%m6-qzdBYe4(z4T{J2kC}Hu{D(c5+ z9kHv74N)W*VU7^6i8gA(7mSW$=Lo*i;kW&ps!y;Meok-ZIO9&zCu)OxoiOu$S(#C% z4#@C1lT8j}wZ!+f)O!2hrZ^LYWC&0q;JdtSii$CDoQ;tY?Bsk=f*Dz`4npzQ0bcLw z9YLm`zaZ)v<)5ft?aCmr499sIy5!@HLUA)?M^7KBQ@40Kh8`wu^&A}@-%1&_sM6FYMGfqs+5HS@d5cQ8H zRpFiEa6hI`EskS9+bH(adEzM#hNrAAX7;9NN&A~dDaW}GLGqMBm9DFy%fAuC@Lj@q z1#`3ANYp$C;0Hg9F#N$@oCTb;k8d8qNbAbN?jK)|+_FF8$81Zce$l3^^{+x`Ojk8J z58CmJ^x`~rV6l+$Px>UG8nXkYp zR#iHIt}p6pf|T6Z^Eayj-=J%5vThN@$;+_x=r~r6LS@j(hi+SOgNvt1w1=BDvv19t zI~q@wK-hy3ns8zfB@9z8k@o58fBA3N4AI_6Umk~RVmaGSX$yXXKMGdgpYa0&%1m#l zH4&i1zx7eE{s-l|yZ{%iW=|C|7%Q(SV)S5l3!OyAAn;D;r(hG_sS1)4zS3-H&L0ms zSyZ+x*wgy)53nB|kiZ=8TzZufR&0uDj`a~K)53T`83VB|pN}^kP#%4zl47#Y%I}DE zDQ`eXqXy|*)LmCM+y>+=QKf&Wzg_Tx0IA|MoR{`-_$+kwBZ@x|8~* z5B?@k$bs&+8k`Kap&KBuV$<>!(0te^y~jM0Gq3_JVouezRw<#oz<1jV&<5#iRN;gY z0P>bTK7VMKqL;$BB_3FVHvgxjTh+=N&(6`e$u| zNms~UfwjELQk-GfdxYPxxBQ(_p^{A2e>!@XW+vW<^>^hR+Q%34yhN-bYL+CjV*B5) zri3`A;(Btr?HWCEF`eOUf^%GFoZ6=DYVIMtoqC&KpN2K$6iKex+^COFkeyA8ZO@8y#1M)(*|7m-?dP%r2HOO<9MclUNLTy>`+^6rCcHm6u_tpM;l3!3UrmM-Xzcz=Sf zcL%3nCn(+J%k#viNCNBc5boZigP8DVNTuDK4=K0&oO=rXAkVCb3TMy$*7pP0GAeDK zw9&iqJ;{9nMuJl3;K_)1^FaY3g2!nSqY5h_D(1gvE|6Q*haBfz+-t%;EF0Y^MgD3J zhlq!*I4i&8en_N;^!-H9=CdfxQYp#_MOj0K1vH+&;;(wt{$d%Pr0VH<8O5+vVf^jO zHlP1PoZ@d?_0A-pX|H;y1x178ZeTWJq#Ua7Do2-EWeZ4P>Ffj%)3mB-LY zK4uoOW^vbbGrhyto#$1g3>u}|c7|iXVih9lF-0%mW_6$`ZB0mrep^sSImf+}TvB4Q zC91{u{#Ha4gU>Xs$pnh>tg3%@6GFQ9wtGX@<*aklGPndt#%MwGoj|w8AHECKG|&BX z(+uJ3#V-;!uD~ayR`+(a_RI51@jo#m5N)vxS8G3cVn7Dw?+Wl43<_rgLxd2Y7>*eJ zBim&I!h;$7j%<%~_c8AZU$FwwpIV;+%;`E4(3Vwy6$_=AQ;w8V`}`}QgE8>ppifD3 zOr?g)!JG2xyDD=~gRswjDL>9>HxpdY8vb5%_1= zY<#J28$?PW^R9rWN<*SEfRz(6pwvDMm>d+gCd3D%TO^?Z;HwJj{`}ex+tUT9+?;`TW ze-zaL69Mh8tWia^oIh#f_dPvQGa^sWHKVaUHDiFB9!q!qZPF%mLKhYT+yr_c{U%@R zWVNsWXg{U|b&SThSRcpTaYumv9cAP9XnD!gH&z`XaoDCN)dC6qL2MH`S!NZRmI_19 z0^b4ZWMmax1m<<@>FvA}mVm5T&Hy9f9|t`mS*-nO`)qeoPw5@3(K${YVt;tkL3IHH zOB!gbP$c}dg?C(3=MOiyRUf|$j8v|(jlE$DogKT3-_a|9Us=!&SnreW1s?fHbQW`P zi#pd>HQ5{w$%TC7$HVH5Oz}rCli8`<1X`tSDup3pS=O`lQNpb=W$jUNcwdenb7xA`8=SpT7_z!EEe`%geTog}Ien^=m^0omFg4$VjF z5@LB2Ejj|-c1}F3YK3C*%Va7V-ND_D*I~o!a%>s+Jqe5tQf`raUkFJlYDsP&)!M$% z_$6}+C|XQ)Z?+Z_(WCLL1N}cB?T=|W=bjZ2u4LkO3ZSjw1eOE#QK&=+ySg8o7qnwen$vq2-+AC`p4)2)8i z(4x`GW^#xn5&ob`uz@K-ld>mIb+%2%9Os)9lxE9-j|X)S!lAw~_`f-qhj0n>zZ}BV z6qJZ%y&+;qqDU-c$kNnv>M6tb@zZZoT#@>#k*H>2KCSKO`z%E{3Pb;NaEak^_1~gS zyXlKGFBIcM!9WLN>LqbjtBtIP(}v2*qtpf)RhSu}tz(SP-eo;|0m7$9|Gw*unKQZY zW#%&sLpjJqhKvXGXw)&az;a)tOgePsO;0}!;-CQHgVQ!K7hL0LCBSkYrp2(Fj}9;P zZ@n!=P8qJfO8GZtLUMl?y!&#fTwzuDMs7yOsKQ1_8_@-bWwUlKy^D&`R)4DpO#XzV z`=bZMaN$4iUf^|7FYOpG=c=%?1un6dsLlGxWJTluo+xdzx<++l8@vnGGFjdv14&{@ zgy&pi(Kv%H%?h~T*wLtF#Tj@)chUIU6QVG z5ofjbc3Y$D7Dksxc+u~n2MtPT4pUJBFFN{Aol{;!mVo9(kyD|0Jav;jJ2MfK1o2;v z2g=Z?4X&u0{A`Pto5ZPb7~v29eZ-jPETiWt*!~+f&mEI0FGUkIU`wK-)!GV$h|?Vj zEdSRRGo?T9@+W}Q0g?u3+}?5WMoND0dXWPl64q%a6i0$Ye|az@fUY_On*M^U6RRm%s$TOFB6!>lTeQtF3Epi zjlnMtl)6$R6;I{UGCv!|6A+!@$IzT`a|58Ay z{gUDiv0zvAzS)onyC_xMI+hOoluqqW5u*fU1Rm&n|NLlc@gC2LF%xJEm3=5t#)Lds{$12+#@Y3lA5qU0zip+zy6t$;Eu3xLt%`2Tf1ov;_~Ker+; zl(I|FDHo)Pe1WXVY@!#4Q%r4Acbns&)y`J<2}mQx3ShgyW_zk3KXt z$-NgDfcP+z-w6{$d}GX7)AFJ_W+8%-o*u7)(d}~|G~dQONbF$&DNQVmP?n)F=y`q# ztguL&%`U>+u@a?GhY7^FRXt`5Cnfq-Q7;L*jys55x+9>!58Y`s>(;Di4QbS#71DX5 zU<)S&G}Qk6akwWm>F7RPEgPMNTP%T4wmyzrS~I!=R4}x)n^4Fqg99js0w|{s(JRZ< z%|Qv!yqKv2N(X(B%EchXwCQa2v!oYae`UNrQSnG@N&(Cqrk&nle_7cLlsRx3H&pW9#{c zd3jT??QOh;F5h!f`kY#2ABZ&h@K-N&o`eH%Qhq1q63VuFA^)fpx{^>29nQ_eNyT$b z)kvh_#r3oPcM%jtA<841+hK&WB|AbgNvjcxJq)ZJ#|D(ZZ2(E6bPc5RQ*;eb$&O86 z1mTY#kz}xKAldkay7tQ(BA9_B__BTkoY_#GZ0WtGaml8MNgvP<-lu?ZivEebfD=gs zh=WS%ig6nqxBXEa`%%%N{6F&+3$kCs+=ID^4oCp+dTvdI1t(Iz)h_xS81BNg8awbW z2-CdogDdDujQm|>WRS5Jrgi#SUj;|!XHVhFj~Yl@HW8E}pHS9NEtX`s>2vD-L%v0y zQpzx>8liNj!Wo<)eaBr$v=(S?1A@COReC^~I0o#r!}l;a1gU(|(txYUFw*{|yo z9rm7l1jvwN1*La7fX-=NoJ`BHuhO36>{urk>3%7BZtYp*p#Sa5)F19C@@1TIO!d^8 zQd#Q^?Fka~(mw*iLP5>trd^<)n@RLxO3LNVXvQ2!f&fs1-fmc1wd~^mHppXISB6!P zuN^@K(I*b3^%Z@@;0haN3M|{>8RY;DaI3m41k04=$mO7R!Be{d{1S_vBlgM(!;^Ye z$!um|P;`fIhj*STMx4aU`hvNSx45M$1H9O#6P0?T06c+vxgtdE`W;vL`^Y+=F#{Sg zSv`iD_D=j(4AP!iJ5TKE_tljy9G8@0_b5cW$}VNb)0JvV@7(B$Q&%9B9G@_6WAL{= zW}^(o;4_5Q3Un9Fa^AGq{dMR5E9GoTMDtprqPXXIHuFd|^ZKRv5LUSXvhsaVlU-$D{dFBgbAVciKzg8&GYf=ULk9^J|EY*?GY z$VB0SC_-VLsx08O93~FJz#8txI#GPA@T)Lv3Uqf`p2o3Sv7XWEVph`pV=1<6P|mOb z-|QK1Kz`L~`y^gFkZeSyeo05iMTOLQwk%$cT z*3Cg~@HTXd3C;jSz8NKIaT>@7^iX&i-|W%G9a<~g!JIft+q+YIdA1 z{wRk}cKT+nR-Z0VC!C=_4?qhbpf@UT0)9#83@R2Rnrmm;r!}13XAi9n?ySMK0rhz7 z1l4h=uF36KpUl@NnaG^#kCp!aE&!76JIO?tf$~FMtb)>8At2nK#}|Y4RiH?fS@A3? zS#S35YHsXqt3=gFka%cZX*W_OD8>u@of>El1Gq9z8AK{$L0v`y%6tj%H2I zJ*Qy=SZ0xXHWS5;w@`gII*u_ENvdP~m)IY3X@udV!zK4dI+}b0KaNcNntfZMY#s!s zd?pCOd4>g-@YY1D6nyyxoV|+CnFrzK+(5(={zz)mIhI|sa%WfDV`T88IjlV>`Y=wF*$)ysb!3Q#Bc*lWGBllM~~ zO-OtrQ|W^8AsgL+?7gQBXW)0)62KK`P$k2k%J)P9gL78n0NW7l7<`OHd_pL4{zJ3) zTL~BK523VY;y)j|q{!@2em9E)z&rB#@+dq6ijuNw_KR&VJbLYBfV)h-U1_(&J93GF>MY^e($J`?#5ptb| z|IJRtiPPE`^mxNAi<}C6xYiB`7q3X15EzE0dMxT}*+LMEo@P z`#7vXWC#ZNzO4j}>jG~~|1o)q6qK+Hz`B6uf#Y%^6aaN<>`8n;%0BccH{MVl|J`fh zap)gM8z(69YX9f|>K^FJ#C(xoFOWm+FIuIOP4CsNt_!*1xZoQsf!1tGEGwFddHGUi zoNn1ra;DQ9?Ll}n6G7veWEvH1p%ypNp+&7a#xY(5v$xD94ys7O%)-Dq|GRINt$8W! z;7l4}U8gTo}(Qz5jI=6xJV>5)De^ zPh9H~)Bo=VMPF8yBx6e4{B5h7reJaX+s8BgzQENI!)FWF87##l(47FWT*tHL(xpB; zR(U@Ib?TmIWl3Ay)rLlhx8kOiW6dY|QvO|%yKP5_UV5$zL zBO2AWLZNZnU0EFf^z??)?zJDbNaYuPPE2P7^6#m$q%xj2(oN}2Rs#eMG){ckI`u`r zecd9aqZ+AN`)Ysxkj`SQjk9~QvrqEt#l_+TFL@Pzs?h-UFwW?*MVo#Q0dOj&f=5+5 zei^z^MX}4X%;ew)BmErz%?-!#glQsZ0lL}iS>MuPRjU~4KW$$Cx;r!&px?NT3==Qw zey^W(o4M=liBbpVGoMzG`w11Jf@TzauMYkb$0TJd!l8T*B)cbHt<4YYdlTFlL8db7tTlc*4m(?s>htN<%wfDc+4t9aBB9 z=CqEob{!d2aXrxf(rKhfQ*JY<$GEo2!7kP0>F;d&zIYqAN!cGiVe5CzV0#Q3_Uj&E1IIlQujO%7_2pcz zs@re1LwJ@%mf;QhT@tn}eOr2+x^8|kTvrl%y|da6J>mJ{`TQU{8)__g=YMg{U$o$> zd)ncMwpeZq*Ew3n>jB}aR$?rGzV5K(-0v0584a90pLl6U?D-^`wpjl3%wL^?wNd(k zp3B@ESZYZJNTqij4-?vxcQKI-oS0QwSUPK(O395kYRBp64x%ID{23wK{BrXzl0IAz|Q*jRBfuw;9#Hy!Yfo0`WZ)kv9MwsdOj0(4#C?l-Vd zlij~x+i>E^*;gDU&pXKEWr@Khk&Lrvz({?sPQ_JxMuE|IE}W?GUEL6h!Mn(iW-vV; zOAf-rY)0vTeP(3Q`*_pVsdE!I{>LdKInR)MYtWujq$K8SH_4ayfYyM|?1fAC-OYpB@xQzt$YM|G&)hd% zqCICUKXBLyP|}bS(lvsW^M$8Dk7sb-{uA;b8Mk-HjN={vzH-mU>a!vpk;ylzI5(H# zI2SVT{MLzj|8mO;r#dx!TvEpM9k=2Z(Zt=yjEMhS-)@vAaeBzV`qjgzL0$@{yspvr zR{(!ZNn~%t@Dp-`@=4|15Fvu3?ror{7I2fQ4&hc}sQr=2DtC{csq~oE+3%b&Ao0?h zfy%ydYSn&_GJEjzzBX5qLGP#k43D9fT>r#B!Dv?IkKoS?QDy*p<5X)efM~PXf2*FK zhu%UDcOSFCxw6Zq?K+~?-n&u(7clQ_Kv`3f_8@580N_Q4cVo*;?mrj~=Lr z`4{YwuPRCz9jCSC?&N(-^KWs|P${o4nY0gbHGg9f3uS@fh&{jHKTDnE=C*%y-)%v` z84gru>#GFdkrbdS`QBC(NL>6_SwU5Y)_~?-X!gQ8SNna?@#eucrF}|wq0x-QhcKI_ zCnbC1`6q=aSBR3KSJzM6gz2kMHx-8FQBu0ErX~kFG>`Vyrv>l4FkkXJ5#6_&(|uJK zEw+1%*Mk^@EMc!<`VTja5(`BLcHD;2h(2?bjxqD8LU2uAG=AUIbOC^-T1Cax66A?C zE>+63h*>Wh!f3`lMTM`Y^Y>RA_>EU{$FN>h!x8hJ0Nsz~t`HaXUEPIuzu@RccyPhs z&vPGd^a*rnSgz*81W51l&VCydkL^&yU4e}Ei42t>(XUcrQ7vvdC}v+{H+L@vm|p0B zVwql}o_F+OXDHb`I@Mh0w_mwc#A;r#9S+bT4s#QbUzSRQ1;xzWb6i?pF{1OfB<%y6 z1rbnIXpEkyu5Y3JPpaAPJ8m?bP0Z&nrYv${+8ADYUWV1fzvZ%e*Og`2K^a&;L!a;= zu8~<2!!-BxJ?pHfMG}SUhXx8IZ<3nKQRwU54gka+P!V@?F?N(Y*!gA$TjimQKtuMO{fI zii=MOXr_JFV(uqNac^FBaKa^tq~#@`0bP#sc%B1^)ZDbyULn%54QT~}NBTz?A6y$X zhk6#1?Ij(SKaC_UXQo`2=>0Lskgsp4N4EBhYE_hnrk7JKVfNF&Uz~Jc9|`8I#>6qx zJMx|=tfBjpU3)H;5Aj;&8cTkaob?fY1K^HGJRw?L9KU757rK9??dC6%<|SZ1lgA%> zTK8HBxBwT&F^{Ln(JVv-;r?;uvwurTB#1R1{;_k#RSE36>LTp^L)~QZi1S z@@@6_ag!PYtiUy|)dU*<)9L#~1gUDbIxDS}vVEEA{NoR!v%oov2mAdD78IDB*odHE z!(ZvPIVR%XhjAB4L%vSe?Y>6S5b}*OAPyAYMaeY3iL}-vv zZ`A{-3_Pxcw%A-7$w?*!x_vD)`z+QD{k?rOIy4{=IGV!&7|kLEZJ4{{Uxh*6#eW#Z z|2)R}nR|9ZA>u7QfEu7YOvzq>uM&+ZF1uA%7c_vy(qDXXaA)Nze`pTeH)Havye=5v z98lmBWc3ZdeV&830QwT=GiB1g&^(%xSJg_h@DW<_r;NRXR=NVJI>@OZyi16nT_>Q$ zasH!1#DeO21Kv?f$U1?;vTZ+4)Po>WT)k!^ciu3$pFrq_veTcI^=4oxKzGkEZB!Vw?ZFTR zohG<<^74W7$2L}{`(4+4*|FQAPU^k8@k+b23Cjg~VB zN>VUOX|LI)S1j`{1(3kv0JY0g`O_l#^ZiEJmaom?>{yP)fWy9yFqQd75JpVVRt}y{ zD=1`3Ymo=o=TfTu|7YH=CKV>f22IoQ<8t%34FuPm27cehU}U@m){KQYB0eq& zHqXq6P`5|zPbRg;fXEu~!v>bDBSjtSg1#?rbn;V}S`JzDqg>GcV%R@}cfq9=UmXT0 z6(>v;@w=c(;W_NTYdThw$?>1(uj+K6_$`-;%b%hC4NttqTQx$=h2*=n*d?saOAMUN z8Y>Hxn$ByO41kc{SI#b{A6Xru%4JM}uS9o^*E@U^aL|k^{-OjmH!%i+Gg7@NPg%wM z#iEXTQkiBj*0VKm%>M8NX#*H5(AF`Ot!JR-7Pm3!Nz|FU@MsO4<{m?v8Gym!Re#=u zpLGDu{#mv5r^5a7#m1Ex(K(9WWNO#VN~(RJ+`b4R5yddcuSYTD%bap*L<@L zRoyQ3k$7xh-|m5ihILD3)4Bk}UJv^YlO<7BM#!Ss%ZeJRuf*}hsveN%2UgX=sZp#9{_Lj7lN zT~{VRuEK?3tCB{)rX}W2cXpD(sF(vtTR(lZ5XbPRAYa_XrxCKf3a+8gd3I*Q!;;3=Hvo96(z9x^ zg=r?fmwe6HpAL=RZ5sW1FDMrdJa_uhD%8&|?xhK-kP_P6$bSE)o*LvdsS{r0pS_A& z<;(YxF=9X2#FA!XvUZ59FBJb8SikUk-deWj4ZT26Tj}P4#jG~oNy_uBviwx=EPby% z(Q<#{-TE&@LCy~EvMO90ZA|EXl!xp3J;HlP+N!=Y!sgWU6$I*msImGy+~w?}fHF;) z4&Q(jCxEOXO~Lpxyjai{WIhQ~Re!iZSmg%-7JOXD*UonwO$9Qpx3+Q;UIp@EpAskb^n0evFk~#-+ z5Hf1Zt>m8^Tm?l3=hc3&h1GzE8Z=CAF&b5k;jscItSW@}od4u!+#6$*K?Z}np-*Co z`%vgQ*14Zu?Ve0pjv2Z*u&nNViF-Mh;p*i-{}lg`{`e*E-V_)b1y;A!DZqjXXTj}W z$|iZjU`2>G)dcvp^OLxcdZ7r>^R(XrNPTLj{=Pp-K(BcL@ItSqpp+}p23{|m3y*2> z*;VXt&(&w#$43YS`I;WqTl)oONH{ps0>I~Jv0u9Nyx3Gm8sohj9BpueG(EFe8i9@B zDyAIpqlCQIteBNZj5$nGwz_J!HK(rvI0wc z;Y3hl#xa?7T=F2~W*h5}U?3!|70?Q82D?I!h<|J+RT zyqLO-S4`TlD7z0{4abk&RaSxtU`H286{IMLhM$m8k9j2M z=D_X?6snaS(g%M>@@778us~J%8Z(d6?w}vxbl!w1{WPZ%Zy>wx4udv!Alt|}DZy;a#~GIO~9ntje$wcVrCOd;f&qR-HLR#Ty1R7mt+332lsUXtke}7?_|-l zhi<8!b=o29_Axx=AvoBiLFma76j(*Anxxa}(g)S0bAZN?&hEuR=AL@BV7-yw!8rz9yU{&+A3W2X1Cb4@8E1!v zJR7Lpp++J4xrw|lYiUmLmhda{B zV?aE8jpdRIKM57uYD~s(pTb2M2kg&hUS5*YrGfc~5d^RjEA)AZlrgop6<1Kbc%Ruz zN*f{6BbL=Q^6w*_fnDV13e*2O9d9z+q9yYl{iPayF`>XYYH#vn2)_FS-$7T)g>dp< zmsr2w_$4w#j5@T7myK0pbe1`a}G;Og9S#2_HYgQY{bnv4(Pdz#GiiJB$HW9QsliYVV$@P zE$$-X22@6`vrXSdtrW7}s6CJw%zFsgsmyXx!g_J-@)UNM-UhW-V%<>3cGwEeqZV28$cNFC&z&m!L#3zT!1`%lxRYkSg8zI*@bB&GF7x>9diB>P2+ zQuV!~_P11)j4ae3mBe-Mr)d4CC@M-@75DBNl5_#Xm)?Gty5>gK`)^Z>wtYEI48&N? z$-M`@`z<`1cviXi(~iu#VIQq*MvVCk|5B>@D0G~ymZsfzd3Tm+TuCC1O-Gv0{kfjf zf$XG-_FGh_z7_u$pY*sdzG8An=tRKt637zCT=^&L70y#it(&*`4O4S0?-u?Tj}^O( z+SpQ|aDVO>&dvEWE$+}5GwW1=M7^R5O=7xDCry!X+p|Sw{yR1itrdt$|e6&jV zQJ&tD-{%z}pZaX7l6S}G&O1c1ndY*!6yD(}c>KCXO+^Ij(c`o@E@K^OnrAjC*4#7TXyy>PvVrb0 zp{fD;>eTxT+HUv2^*`Yq%3;*6c`C)WiHBJgOv6K7vQaD@?W zfN-x*fAdmlY}d+FC9~>XOHa=2qtokMeA~gHUTIKw*fl+tT!Vob+VG4_r1~Gjk6Griz&+y~(8+1; zhA9L^oMj+eDOE0db9s+sfp(qR{kx)zVnGB| z1G`$Gmsm94CQd$w_bd@krS* zmCkGK_&WzI)o!Pswt6X_C3oGjod`cfO}SBM{{@@Adg%wNdS0p@TW|bFu&Bn;MZ=H% zCp}Vt(k>siz*`(Rj#TKA26Z^omT&|>#uM1V2R8S;^W#TfBh0LGe+E^vt za(!qW>pee2fti}De0J}8Txigunr$VqDM`5#^IkiK`;YOcPqxQG&lY^($u8sNfFbdL zQ1S@=PLUy=Fb?UC@uWQ|y>GAI?Cq?eN}=%@nK=g2a$-FrJbrfFTAJl+r;3|6vIZ`Q zpq!qE6PcOB73Ws-@GDV!Tq@SK$%9(0s=0Gw+NiF>+4OpXpiUEKdHvEA`jd|&+Rwmr zwgv%iKQ0X9+@!Tie_e%Vp>FiQa67^45D@uLJhS7&9D*?f>w=psIxZQpw?-Wu{45Ty zVCDu_*MB7WH=n^gTe(po_1ED-P(@i`Lfz6vi4=lm}d!u1R|oV!lCsqNOvzm8N}fL01bz%;S9=uZ@`N`rVr9 zmtjw&{v@@Sjge00P2_r34SEOH6h}TwsrrNboUKO-ygq!-_JeI^V+Z1^Lo9W=N^zUTGo{NR0>r| zYE3qW)!ehknpf-G9a6C?f8G704?kMvIboR{QB$$OIRogJe}sQ52-$7ocG9x_6WqVF zQ=XUaqu=Wvt2j!x%h^WMuOJ?2HvO!%&8dXd`!-uUNr!TE@BXw1_275;KFYsHqaGs3 z?)US)^*x%~i)7cS8xZjXKBCr^K-e>dw^lF}>ba$Z(QMaRhuePV%I9dNxc46OWHdIU z;aG=mCB9>5yhflLSuCT`gbA)HAKqUD1$O`5G$GZKXL{D<@hwi@ZYuBoZTnUl)|_yi zA%Mlxcdjlg@lUj!-P=QlPZpH8FyjLGoV{_go4<UL0`ky_0K(x)Scmf0n$c=5wslkGL5zt?iR``)jaW&|Cyw|AiJ) zQ8D&h-raP{AR&#WFAGz^Ug_>N&Qm(AA591i*R32o>57z3oa+!1HedAIqAlc9I{c}Ie0hsGIUywi z_l3M+P4`P?%75M{NBgtZ$?2WrD6g`&Wb4Y^Mw>&u!nXe;7y9R2o^l#};Al`frwy>} zt)P?IVVknk7qa+LJh!TxxXl+%}$qNNYh z=={K8u}s?s0$m9Z5B`mEls96lvF3#BhCkctP;<1$YdC}|9)7UN9j--@M{>tVIL zYo5tp{EMQv9%RcLIr((!gp0EM>))>>Gj1^yAGdRQK#cf%OY#=<8X4EzlQxoctC+`H z28z=v(Q`n0lLllKFj5|cWsqfyZ&74J%SueTzwE4fc=_#nQNQzRg4Ys9quei&4>sSw z_qRx#*H9|Z8I{#LjiP>Ku>QW$rn!5;D!S6RY;S(Wo4$Hl=|e6RD>M(6>=^l^q(TVXO{%lym1sL-6U&7~BC5iaoH>)1Az+CBF5tW$h(nW50?y^W`mI&W=dx1e6|A}^$@=%Q7|5RSp zXJ;_S@F7?3^{MZe$?5Gcg)&{cmWlI*9~aeK{N{AaG^a})uf9G-h{M#MPiaQydhT8O zNhK%m*EgNCFx2yh$dz_pn?bEhz`vLE=0wbAnUrYt^l&A5acJ7pwxSy1;Y-atjasSC^l#45G`~8g6WHQTB=-P6w_Pph(x$~vTdUR zto|eC{*^j!t*(-#ryjq@+&@&^CwGMWzlcH;83|GwguV#4>iHZkA_*p0=HFP&Oa%q@opy34x0}i{7##9 ziAm!wxBhh@xwawAd9czQb}iIL;0+SG!J=>XkmBJpy^v?p{GUbh9S;eG<7`LQi5AO} zJRenV0IBg(UomLC>@Sp4tz6*$fn>8~s-7$Gc4<_0u(gizP_@uW-FB(pX^@#DY9a7; zL`ttJTh?WBH-_aqoyx6OQXx)+Uq*kV3+b0IdX0?%g6VzKc4DGa2$=_}3s91d#Czc& z4~wVESM=k*HV#)GjxaD81_xDuNxc#-I;e$vI(!e)t|eF9hI*O6o6Sf(Y6 zAt4725l!72-(WpD>OU`8BnzFRRiU2dHb7q)+MRw%@_Scby-(yC@PlZ{_Iy%!!3fD5 z!`kj;vu;>5Asc75Xu_J2xw03>ekO9rLFXR)sU1EqZ;AQNqx!p4l%+4!{L=^92S<-j zCMDLLbdKBHJb>DYGDGw5N5iB1dJGTTB9domNb=BwWaB?nAFZLxKU1fq_8YSx}vx3yOBQG_(yOn)$V3IIhdj_sP1ZL zgNSSDBZbY2d%^3{&USC~GgF&wC~?W)fR#yg%JCrkzfQaE&^C=H-oO493~L0FwW?UR zFbYF-v()`p%lA)uLRgU9KgTHM**qXVyqvOeMy-=8;g!^Bq36QYPOkJ83y2zqdg>#{ zs`vP4OU&w=eyKHFe$9P3XlmK_xQ&#er>nk|B~hP1j>L3h zqys5N352!vu^EjT}&7?CeaWVazP36%DrLzT7+Tm>u@HDQleU?YDs9KBgyuT z6iA22tW21rPB;U&(@&R}?iZYnorY6KH3eG~W8ZH5SL1 zD;QJlN!C2~lBt7Rb47SP4rpHI=!YT%RmfG}r*Hasq6HC90f@A)Usp1*Uq_%x^?7nL zj-&XX-~S%9gnSmu5hTDO-pm?$wQG0UdVL6*C=?W7lJiGRU{hqALw>kv-JlSq_8{lV z#uKkA_sEdRZ2wDqG6H|@IQi#!~S~S8ZPzqU&IN?YieYRZ|2RQ<&oqv+?r>-d4KhC=5L~w(Rt6e1%*KS zW`Be+e~ilV6}FBMB^nUFLySh{yz~w}jgqql@j3}`n}(^b7qgv;X_(W`+-H*h1?U$1 zTH6Wz)DpO^nr8amsk{fLsNRz@PO}Yc`0Hmu;>|;2@m4fgv+&YdMt}Lg0x)TbrJP!R zRNj~5@p86m&4u!kZuRsY{{MnCQxKoMF}wP@K{U&CB?G||OPE3WDS9l*;9jL+OqXUI z-Fc3L=L0a1`$-)fMGv+}L)i1i1Cdvp*H%2Jgx}x$rO~9ZB|=+jT{)ZN zuW@Iwm#RY~n6btEZRpVDQw@{#AcA?aV{@r?Sj9wz=pVYp712lN+X`jV*#EE{F6JRa zrGg1ivrl8bmC~puC3hQZ;tNyPn`Xp5x@F1al)3gOZ!>NQ>v?{rASjApg1}p1RYtHx zrQOI!>yQ=gH1ubc;cG;>?%a-@o8v)c)AWcCreb$DQo-brx!3q{p`XM=x+=X&?48mr z|9wib0m*mRc!z$KcmGllH~Fg1AF0s#Q1S@-Iuu8}JIc)J9c*XQ|D? z99!PGM#O}r(;Gqj-&ZOOwbr$)Ny;-Y#tUJ5oYLpv!*>`$zBQE5++}<^vCJNG_%kiN z?IjTleHd4A`YW0XC;49{^V$nO4auJmL%;aNze7BKMALSoom()W00ZsV>M~2jCwM$) zrO*{EpQz~mNkJ4Q1m2l92wo8Z@0w-xM#vsiCT$!b{b|9L&6V{m-qQCT&+YbyQ8v!L ziu^ZGF%b;I8W*rPq+qBGobJ1u|E}#-{`b6STj%|kEy0CS;yC-9l{Y^>O+D(X|KY8n zw;)`-lY-cjb^75;qI%oRL83?NWntOPj<|% zx3^&jvDCG5$bkI@kah_(hVNG z4lhpFn|*XBn;)ni`}SM!CW}7jSCjYW7Uv!QpemIwJ6@jCc>-zVnUo+?^=6fWWr0~6Y;LsJ^Mo{RWVKxoV3*vX$F0>x5SEC1gzs8S ztarZ!89bHB9=0I`vFSzL0dA|A_j=j+H7EXpJZ~#Fcwmcf5`pz^-3ytWI?&}QeBtFs>dx-0S+b_LLeW>f zNp+%4GTZX~@_tk)YrqP07Y=d_G$+e!gL|TV;`4Bc(q0>^;tIxyR)ok9y+*l@`#`tu zW&7LwQ|3))mpjce(jAOCq1!aPYn;lql&PxkYPBUMLN};FppE0JR~`fj2u<4L*pv83 zRn!jKA&SwE;Rm2hGXG&(i-dS{;SpjTDSE$;q5;x}+~+bBe~`OYXDe7~8q&<2`Pye* z6w>#e3bH>}m2`bS`LBC6hNhL$hh4^>dmy%*iW9sr=hBk#fLp|&hB;l>je@(O>+a$M?n_eU&;(&^^o3~152gpMqW%w1seIi7pb ztTI(?QdGbOII8QvYiLudY0&%jsHQ`}y2;d5g}c*!A93^4zV1;(!&T_HP#{*4WNwK9 z5Mv_9TvbpXPFY7zn$cJ86)c5>#g!+gGpeGTPOr8%L82|@kF|w&sAy!f=&6*1Kh)eF z{A6xZsras&M&?*trd+d*eul5?gxdt?+`!j*hgqNVlW}_M4O8$V6AuU{H1S?{Rrlq) zW$L6SgXE`aDptS$AvxI(Rt6%sL;sQfDJbe3Tq4^I}02A`N7cAPt-qG4f-BAcb-<{VwB?G{HU=^ItRbDYGYWcUtG2 zfqyZ5fsXLXEkZVTq;CQfe%SNf%QzQ3ffn`wF1<3J5B+F6*`p`29+#HQ7DMEHExtHi zF$rm;7veFBV$zhu>-3t9V{=01Rsf94e~-* zT$sCV%KH4dnp37}zm1*xz~0Tdar_jAnm2U9tuHheruf5JSEi(k_M`n4Gbwsr_+`pX z;rnt_9raOE9fiF*4E#-n%>QaXznOSc{f}w1cp7>-Or|}rRHA$leNn;k;$7@rkrt%) zi}^N+3U+J6Ma`x(IDu->zF#kA=QOR1{}hC~G@Y+h-9uhbQ%PldAw z$xZvrlswktQ?(Ab2te>XXdv* zzHzEP#u8*uuQROfXa%tn-*j@yIC4^G$vO-kyf;d^q^J)m5sZ+N+Wv`}q0CrKX|w%Q ztbGdc$8BE!HKN?ZB>P%C#*`>oe7z@E3e*|ycgm8e-Wl3eA2!_{6AorYlE;>>W$!ku z1ZW!_)?Hfe*88wyVpn{o0-AbKOsLurUFZflU+i2O$V0 zWGn<;#!4`c2_$O1x!6J75!tc^{}&5Es>&*>QEheycR5usMv?MjV4FUeg);^2^Z*cdh*UfDhu6JrWRr5e$d zy_kHZb>Ztt2N#%#&_ley$ti!um?%)Rixgu)}L#BdMQ+o&0@D-LNu4dcZAs{>?JkcW$i{peI560~j%h*DHhgtzd4# z=dr3`$G^t9XOU7MiPk9MT1%{bT|u9~7>Z)+9Fv_5&U!S|oMy}j_7dIx5hiQb+Mm(l zMvJ;nd6;Aa4mO_4YWjMVIFH`s@jwV4joH-tKywZfCb8ART`mmbpwGf!)r+q6=hbIk z)Y03$4$8QU zF@j@y3P~Iv4?S44y*os2%~9e1dbidix9Jz8L;pRVU`2CGF&@QmmxIum<+GS0q@CTg zKpxf-+uEU|g+lcw*pu!EUO9>AV}T0#;KexBWy+G3a{xha=bCO^>2L&Lh=zINMP>+E zTcmVf@G>0R&OZbomD~`N5tPt6oqu-&0_Q7mT)X}im2^&N(+mAZJn^0?xGj64IkF>W z4TzoJ(SjlSzN;@p;Rk1^j_|!yVt=JXQmrU)!?(D}&aj^bc6xJlcw>zo1~rZ8lJUU5 z6{4xuVARvK)(DqF?2NyU@`)3RYe4Mwbhi!D0($FD(HV+Qa! zP9{#+#`hu>kY;S+6xMjU#_Txi)r8h+G#p~DB@%xwaxu(_zS=?p-}gXRNpPoy`b2O2 zfQb(%LqDN#B!jq~^bdt}OG8=bFt4YNW9tB%w*f0;YR+?HQF0UOTl8r23l$s=wxxt_ z8-ecywNIF$DTqAxGYM$;7k}PAanw;-+hEe!;NqBb{1mpfmHI0GeUyI6yuok+(DiX3 z)%|n^Qc98m>FWV@@OdF@{e?{B*!U28EubkiLTi?&4Gx#g`K>NAOsJ@KKatXx8qgD<{yBSt(~_>oC_{D+;K=%zfKwT&nG}PXMtJz6wm!T`>r(0!!S)wD$rM+ z_mObGR0D+i(~E&bB4{xX#($fX2xyqc$WQldONsd;!XYSgU6p5#p$oUwR}D=Y9x&cK z1ES*WyKAkfo!H(5g^}}0KAE06RvO2@)YzO(I?A3wkX|ce1;oR4Hy{}JsryQg?&fP< zXmW_O;D8GLkmBNP_h9=&T_wHw1WjOf&sF@&hF{VQ8hywL0l373sVDxx>cS z3_}G%Ka^`-s5oGFAH#(+RvA4%PfflL_84}Fq#z>F7M29O*uicIp#FwBxTL_f;C%Z{ zvS}WNMpVj)Cg4I7kY?kXuC-R^3lA6_vq#b5tz%U-RjLXn5kk=r!Sw%Xsa4bBb#he=JYnlA*Eu2kS%nc#-;l|Fb43pvuQchQRk7zk#zv5c2RCQ z%fvdvc(W{L+6bWjshSyC?I)^x^FYe&4_Y(>L=6o=6BFDiR@E*4vUTME6+g8N)$)q9 zA)LbwC!AM2=6o!^>55iLCqUsv`C4wgfx>=md<sACjWp;FD>kFZe?^< zfOY3pO!6u6(o?$g8>jwb10@e*Nl=xltK|roO^B4tk5-`z#>AW9$BdnU#rY``|Mktl zm3MC84tVxwj>21hb2soFvS36)Vkw%$cQp-$eohwuTH{(lhndPk>r?a-#p3Qqb9+)(l_?THK$NySBVv6;hEo{?+8+AEV+XqJf;`!HfRi>jM z^fnMpA4D;Msj96qG@s(JIcs|nNCp^l=?2(T32$COn{&b1>_@|a5pFL65-q7&IxQ9| zCdf@0Vc|(PcD?uOeUgX%anxuG1o^fL`!P78j3W>(*HJiWEI!o=fgrx0{YQY+L&Y)Y z>`%>ENGVVXG`>_~+L1_f+m;f&J#BCI!ekTJL8O#97?I%SMK4_cniLTPJbjdtKw4_=#00N*zFF=Dn{qGr^aV6gXl$4Z@`SM*Q{Tc56g^$KJN*u3w_$O)*O&_oEtQ-(ekfKbvzuYwq>!{v z6!-yLZ#Ga~A1ZmvLu_TID&hAJM*F4~o`4*Yc851<00B4W(KVvGJ&;a4@%Tj|f`BS_ zUBT$V`G6MsFqHFi)3mxwuHL1Cb0|U|!Fa~8i7Oa=cs7XTeAwk@dhj_ZF{M@=Hv%qf zB`i6UStTW4>Z@0Btigk^^vHrH4^{1~hrF}dJ4{_U&y95eloL8;o6zcWPeG^J|cI6@yjHfN#J!cxqk<^zBt%A^3xSy8BkAjvH2%z#EeF*Y0n zS$s>X3R5}_h%I>uY-#CQdp@ZFI%L;#+r5FcfN05bFdm&A@&<$<`5+U>|NI|uOV#so zRb~+ZQ*S%+)^8^Ujsb=j{xxY>w?m58oW@^(LN%fGKNrT|1I-yoN8qZ4cP~%sUaF_4 zENGOkLc~+JC1sp(5DNf>C0({GATZ&WsBI*GX+i)YY0w_OTLZ`-e3^)cx z-a}|pxVFa7+mVF9r2I9Y1M`=Y{Saml&&@#p0G_+){>-Q!XHXR1I?bo;d~%&Nx(Mew z^Dtma>Gg@GIoPP(0(d?<8zMGbiQV|dl-~CtIG;YZ=YadFAt0twx{e{_JH;zKyRON? zun0TzTYZfcLK}Ca^a9to!)bog9Uhn zyXcU-6gHacApAo91zg1=e!e6?a@yBFH>Y|$z-nQSCs#Fh8-#`FUKwRQIQtX$Gig6EPF)yx?s`6mGz1|9 z-o>Qg8uarchW}T>DG!T%M1L1LT^O49s=xv6 z1S~*%(cd3cc2tgt5y{3Xtb6#6&2en}nSkG5%Z6w+>Q_u`d~CsbmI~=}Je$XpLQD$S zCypXL`2P&k&Hq<2GL6405$(Oy^`B4xixR;fg<=D3637Z%kpR>UM{tyi9CKtCuGG4a z=7eqpegQEZw{i5MYl85VYIp2wn9$_t!fP>%IJe^Ek@G z6A*2ca%hS&m{ujtUjow`=RmE&GlorE#u&mMAOYfrH_w}r9_FcSnlz;};J#}-vmhW} z10{p=wbd-O>9>!0C%;u2OQg(|fd><7xhJF)wHvHwXAEyCd<3n8HAf%9RP9arALTcILVimKTuubw zsXRh5rkkdI!k`4A&#w7JQp3fEXGC}lLQ}YM#uaWdbhtCCzd7gIQ~{AD`eWg#G4DTw zJvBI2pqyN56BZk#iGFnJ?ZFoDdw*KiX!CyoXpJddMqxIvy^*3rs0aha5clSpEojmJ z%%oEfr!nnRSWN#Y2F#q8w8b=m{^{5Ys5t(tX%K(bE`$mgTE4bk4cafRQMa(KZKi0I zx}~e52@e5*#gV`qcpWGW8|=>tKtIIut{iI;Zk!Epf{bls0Gstcxh)0oB07%3_4bLH zA|4GC7A?z1nM3)ug(uj0i~lP($d%XrU`ERV+nJ9Wa}Kv~I1~tdYZ&iNVU~P;w&|xV z-Wp(+|3*bw@~%q@wQ1cUj1f{PvCG%Hc6pKhJv@q~9*ztWzTM;mv!a}X~&;fy*SuC+ZfYoQaN$(8AW0$Ul^ zvHzG*PG=LtedZ-P6RRgkqjMY{WA}>eV-IHT%zQzjf1UwL3C=Sjyi_*Sx&a0iJ=n_h zUB}Tl;SdyC=wB&8yoU=Yy9or0ES!5G+*y2I`{Wm3&kx{PQKpem+VFXw6odlo!UF-t z!RCygN-!0E=~M6mAK-bm#fkQnKp74EdGN+`XCR?KrK9%=BUuKt zz1+(-Jr&#(Lc?>EbqT{5+OYS>Gg}zgC#(7Z@F;fnOPf(4TSheNi;<{t*9kU3nYuM#INB%OuTe_mqPQ#rU4QS z+CSkhz@d0C@-^vNCzh`*Q*cdt%aXQFMI)mu_hSfn^N9M4ac@l&pp~2n{|WQJR2Xf- z1LU|=I5SVs;xB>T-%h2rDj7KpGtb{XgwoH1aMG;#iTi$Vc|t!j66*^dAcfwfPV6SiPYThEbuE-J>$_<01UY$j5^$YuknKq^HxKi818-g9@8Ow z)Ryz1K6li;9RYMND0ILv*W>pj$`oaftu0gXOo@8+4c&cecvo;jLZ|=v0X%qLeS?0b zPErrOLE(OiQ((7ibO32|Szunv+O6N?Hue_yZjj27{Uf~QK zgbT2$KQ+tDLxoq4!~GD4@|_7&rBZkqo->?^K*S7(`P`7VihS8xQAWuzmB;y!UBp?!hcb_T$^GOKf^V2xI|1);* QuV4_J8-~{_&~{<}1KNHTFaQ7m literal 0 HcmV?d00001 diff --git a/frontend/src/app/facilityData.ts b/frontend/src/app/facilityData.ts index fbad2eb..1ebd43f 100755 --- a/frontend/src/app/facilityData.ts +++ b/frontend/src/app/facilityData.ts @@ -31,6 +31,7 @@ export type FacilityRecord = { golfamore_data?: unknown; nsg_data?: unknown; vtg_datoer?: unknown; + vtg_updated_at?: string | null; social_links?: unknown; course_statuses?: unknown; footnote?: string | null; diff --git a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx index a6966a8..6db4bfa 100644 --- a/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx +++ b/frontend/src/app/golfbaner/[slug]/FacilityDetailView.tsx @@ -526,10 +526,18 @@ export default function FacilityDetailView({ facility }: { facility: any }) {
🏌️‍♂️
-

- Nybegynnerkurs (Veien til Golf) -

- +
+

+ Nybegynnerkurs (Veien til Golf) +

+ + Se alle VTG-kurs → + +
+ {facility.vtg_beskrivelse && (

{facility.vtg_beskrivelse} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 1dcca23..0b4c305 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -18,6 +18,11 @@ const displayFont = Oswald({ export const metadata: Metadata = { title: "TeeOff.no - Din guide til norske golfbaner", description: "Oppdatert banestatus, priser og informasjon om alle norske golfanlegg.", + icons: { + icon: "/icons/cropped-siteicon-1.png", + shortcut: "/icons/cropped-siteicon-1.png", + apple: "/icons/cropped-siteicon-1.png", + }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/frontend/src/app/vtg/VtgExplorer.tsx b/frontend/src/app/vtg/VtgExplorer.tsx new file mode 100755 index 0000000..3872503 --- /dev/null +++ b/frontend/src/app/vtg/VtgExplorer.tsx @@ -0,0 +1,661 @@ +"use client"; + +import Link from "next/link"; +import { useMemo, useState } from "react"; +import { + enrichFacilities, + filterFacilitiesByArea, + HIERARCHICAL_AREA_OPTIONS, + parseJson, + type EnrichedFacility, + type FacilityRecord, +} from "@/app/facilityData"; + +type SortKey = "soonest" | "price" | "alpha"; +type TimeFilter = "all" | "upcoming" | "thisMonth" | "next30"; +type PriceFilter = "all" | "under1500" | "1500to2500" | "2500plus"; + +type CourseDateRecord = { + dato?: string; + status?: string; +}; + +type CourseDateSummary = { + raw: string; + status: string; + comparableDate: Date | null; +}; + +type VtgExplorerProps = { + facilities: FacilityRecord[]; +}; + +type VtgListing = { + id: number; + slug: string; + name: string; + city?: string | null; + county?: string | null; + vtgPris?: number | null; + vtgLenke?: string | null; + vtgBeskrivelse?: string | null; + vtgUpdatedAt?: string | null; + nextCourse: CourseDateSummary | null; + upcomingCourseCount: number; + hasPublishedDates: boolean; + allDates: CourseDateSummary[]; + enriched: EnrichedFacility; +}; + +const timeOptions: Array<{ value: TimeFilter; label: string }> = [ + { value: "all", label: "Alle" }, + { value: "upcoming", label: "Kommende" }, + { value: "thisMonth", label: "Denne måneden" }, + { value: "next30", label: "Neste 30 dager" }, +]; + +const priceOptions: Array<{ value: PriceFilter; label: string }> = [ + { value: "all", label: "Alle priser" }, + { value: "under1500", label: "Under 1500" }, + { value: "1500to2500", label: "1500–2500" }, + { value: "2500plus", label: "2500+" }, +]; + +const sortOptions: Array<{ value: SortKey; label: string }> = [ + { value: "soonest", label: "Snartest først" }, + { value: "price", label: "Billigst først" }, + { value: "alpha", label: "Alfabetisk" }, +]; + +const monthMap: Record = { + januar: 0, + jan: 0, + februar: 1, + feb: 1, + mars: 2, + mar: 2, + april: 3, + apr: 3, + mai: 4, + juni: 5, + jun: 5, + juli: 6, + jul: 6, + august: 7, + aug: 7, + september: 8, + sep: 8, + sept: 8, + oktober: 9, + okt: 9, + november: 10, + nov: 10, + desember: 11, + des: 11, +}; + +const currencyFormatter = new Intl.NumberFormat("nb-NO"); + +function formatCurrency(value?: number | null) { + if (typeof value !== "number") return "Ikke publisert"; + return `${currencyFormatter.format(value)},-`; +} + +function formatDate(value?: string | null) { + if (!value) return "Ukjent"; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return "Ukjent"; + return date.toLocaleDateString("nb-NO", { + day: "2-digit", + month: "short", + year: "numeric", + }); +} + +function normalizeWhitespace(value: string) { + return value.replace(/\s+/g, " ").trim(); +} + +function getStartOfToday() { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return today; +} + +function parseComparableDate(raw: string) { + const trimmed = normalizeWhitespace(raw); + if (!trimmed) return null; + + const isoCandidate = new Date(trimmed); + if (!Number.isNaN(isoCandidate.getTime())) { + isoCandidate.setHours(0, 0, 0, 0); + return isoCandidate; + } + + const numericDateMatch = trimmed.match(/(\d{1,2})[./](\d{1,2})[./](\d{2,4})/); + if (numericDateMatch) { + const day = Number(numericDateMatch[1]); + const month = Number(numericDateMatch[2]) - 1; + const yearValue = Number(numericDateMatch[3]); + const year = yearValue < 100 ? 2000 + yearValue : yearValue; + const parsed = new Date(year, month, day); + parsed.setHours(0, 0, 0, 0); + return Number.isNaN(parsed.getTime()) ? null : parsed; + } + + const normalized = trimmed + .toLowerCase() + .replace(/[.,]/g, " ") + .replace(/\s+/g, " "); + + const monthToken = Object.keys(monthMap).find((monthName) => + normalized.includes(monthName), + ); + + if (!monthToken) return null; + + const monthIndex = monthMap[monthToken]; + const dayMatch = normalized.match(/(\d{1,2})/); + if (!dayMatch) return null; + + const explicitYearMatch = normalized.match(/\b(20\d{2})\b/); + const today = getStartOfToday(); + let year = explicitYearMatch ? Number(explicitYearMatch[1]) : today.getFullYear(); + const day = Number(dayMatch[1]); + + let parsed = new Date(year, monthIndex, day); + parsed.setHours(0, 0, 0, 0); + + if (!explicitYearMatch && parsed.getTime() < today.getTime() - 7 * 24 * 60 * 60 * 1000) { + year += 1; + parsed = new Date(year, monthIndex, day); + parsed.setHours(0, 0, 0, 0); + } + + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +function summarizeCourseDates(rawDates: unknown) { + const parsedDates = parseJson(rawDates, []); + const courseDates = (Array.isArray(parsedDates) ? parsedDates : []) + .map((entry) => ({ + raw: normalizeWhitespace(String(entry?.dato || "")), + status: normalizeWhitespace(String(entry?.status || "Ukjent")), + comparableDate: parseComparableDate(String(entry?.dato || "")), + })) + .filter((entry) => entry.raw); + + const today = getStartOfToday(); + const datedEntries = courseDates + .filter((entry) => entry.comparableDate) + .sort((a, b) => a.comparableDate!.getTime() - b.comparableDate!.getTime()); + + const upcomingEntries = datedEntries.filter( + (entry) => entry.comparableDate!.getTime() >= today.getTime(), + ); + + return { + allDates: courseDates, + nextCourse: upcomingEntries[0] || datedEntries[0] || null, + upcomingCourseCount: upcomingEntries.length, + hasPublishedDates: courseDates.length > 0, + }; +} + +function matchesTimeFilter(nextCourse: CourseDateSummary | null, filter: TimeFilter) { + if (filter === "all") return true; + if (!nextCourse?.comparableDate) return false; + + const today = getStartOfToday(); + const target = nextCourse.comparableDate; + + if (filter === "upcoming") { + return target.getTime() >= today.getTime(); + } + + if (filter === "thisMonth") { + return ( + target.getFullYear() === today.getFullYear() && + target.getMonth() === today.getMonth() && + target.getTime() >= today.getTime() + ); + } + + const nextThirty = new Date(today); + nextThirty.setDate(today.getDate() + 30); + return target.getTime() >= today.getTime() && target.getTime() <= nextThirty.getTime(); +} + +function matchesPriceFilter(price: number | null | undefined, filter: PriceFilter) { + if (filter === "all") return true; + if (typeof price !== "number") return false; + if (filter === "under1500") return price < 1500; + if (filter === "1500to2500") return price >= 1500 && price <= 2500; + return price > 2500; +} + +function statusTone(status: string) { + const normalized = status.toLowerCase(); + if (normalized.includes("full")) return "bg-rose-100 text-rose-700"; + if (normalized.includes("vente") || normalized.includes("få")) return "bg-amber-100 text-amber-800"; + if (normalized.includes("ledig")) return "bg-emerald-100 text-emerald-700"; + return "bg-[#EFF4E8] text-[#556555]"; +} + +export default function VtgExplorer({ facilities }: VtgExplorerProps) { + const [areaFilter, setAreaFilter] = useState(""); + const [clubQuery, setClubQuery] = useState(""); + const [timeFilter, setTimeFilter] = useState("upcoming"); + const [priceFilter, setPriceFilter] = useState("all"); + const [sortKey, setSortKey] = useState("soonest"); + const [onlyWithDates, setOnlyWithDates] = useState(false); + + const enrichedFacilities = useMemo(() => enrichFacilities(facilities), [facilities]); + + const listings = useMemo( + () => + enrichedFacilities + .filter((facility) => facility.hasVtg) + .map((facility) => { + const summary = summarizeCourseDates(facility.vtg_datoer); + return { + id: facility.id, + slug: facility.slug, + name: facility.name, + city: facility.city, + county: facility.county, + vtgPris: facility.vtg_pris, + vtgLenke: facility.vtg_lenke, + vtgBeskrivelse: facility.vtg_beskrivelse, + vtgUpdatedAt: facility.vtg_updated_at || facility.status_updated_at, + nextCourse: summary.nextCourse, + upcomingCourseCount: summary.upcomingCourseCount, + hasPublishedDates: summary.hasPublishedDates, + allDates: summary.allDates, + enriched: facility, + }; + }), + [enrichedFacilities], + ); + + const areaFilteredListings = useMemo( + () => + areaFilter + ? filterFacilitiesByArea( + listings.map((listing) => listing.enriched), + areaFilter, + ).map((facility) => listings.find((listing) => listing.id === facility.id)!).filter(Boolean) + : listings, + [areaFilter, listings], + ); + + const filteredListings = useMemo(() => { + const normalizedClubQuery = clubQuery.trim().toLowerCase(); + + const filtered = areaFilteredListings.filter((listing) => { + const matchesClub = + !normalizedClubQuery || + listing.name.toLowerCase().includes(normalizedClubQuery) || + String(listing.city || "") + .toLowerCase() + .includes(normalizedClubQuery); + + if (!matchesClub) return false; + if (!matchesTimeFilter(listing.nextCourse, timeFilter)) return false; + if (!matchesPriceFilter(listing.vtgPris, priceFilter)) return false; + if (onlyWithDates && !listing.hasPublishedDates) return false; + return true; + }); + + const sorted = [...filtered].sort((a, b) => { + if (sortKey === "alpha") { + return a.name.localeCompare(b.name, "nb-NO"); + } + + if (sortKey === "price") { + const priceA = typeof a.vtgPris === "number" ? a.vtgPris : Number.POSITIVE_INFINITY; + const priceB = typeof b.vtgPris === "number" ? b.vtgPris : Number.POSITIVE_INFINITY; + if (priceA !== priceB) return priceA - priceB; + return a.name.localeCompare(b.name, "nb-NO"); + } + + const dateA = a.nextCourse?.comparableDate?.getTime() ?? Number.POSITIVE_INFINITY; + const dateB = b.nextCourse?.comparableDate?.getTime() ?? Number.POSITIVE_INFINITY; + if (dateA !== dateB) return dateA - dateB; + return a.name.localeCompare(b.name, "nb-NO"); + }); + + return sorted; + }, [areaFilteredListings, clubQuery, onlyWithDates, priceFilter, sortKey, timeFilter]); + + const stats = useMemo(() => { + const withDates = listings.filter((listing) => listing.hasPublishedDates).length; + const cheapest = listings + .map((listing) => listing.vtgPris) + .filter((value): value is number => typeof value === "number") + .sort((a, b) => a - b)[0]; + + return { + clubs: listings.length, + withDates, + cheapest, + }; + }, [listings]); + + return ( +

+
+
+
+

+ Veien til Golf +

+

+ Finn nybegynnerkurs i golf +

+

+ Sammenlign VTG-tilbud etter område, dato, pris og klubb. Dette er laget for folk + som vil finne et konkret sted å starte, ikke grave seg gjennom hver enkelt klubbside. +

+
+ +
+
+

+ Klubber +

+

{stats.clubs}

+

tilbyr VTG på TeeOff akkurat nå

+
+
+

+ Kursdatoer +

+

{stats.withDates}

+

har publiserte kursdatoer

+
+
+

+ Billigste pris +

+

+ {stats.cheapest ? formatCurrency(stats.cheapest) : "Ukjent"} +

+

laveste registrerte voksenpris

+
+
+
+
+ +
+
+
+
+ + +
+ +
+ + setClubQuery(event.target.value)} + className="filter-field w-full px-4" + placeholder="Søk etter klubb eller sted" + /> +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ {sortOptions.map((option) => { + const isActive = option.value === sortKey; + return ( + + ); + })} +
+ + +
+
+
+
+ +
+
+
+

+ Resultater +

+

+ {filteredListings.length} VTG-tilbud +

+
+

+ Vi viser klubbene som har pris, beskrivelse, lenke eller kursdatoer registrert. + Resultater uten dato kan fortsatt være nyttige hvis du bare vil finne riktig klubb. +

+
+ +
+ {filteredListings.length ? ( + filteredListings.map((listing) => ( +
+
+
+
+

{listing.name}

+ + {listing.city ? `${listing.city} · ${listing.county}` : listing.county || "Norge"} + + {listing.hasPublishedDates ? ( + + Har kursdato + + ) : ( + + Ingen dato publisert + + )} +
+ +
+
+

+ Pris +

+

+ {formatCurrency(listing.vtgPris)} +

+
+ +
+

+ Neste kurs +

+

+ {listing.nextCourse?.raw || "Ingen dato publisert"} +

+ {listing.nextCourse?.status ? ( + + {listing.nextCourse.status} + + ) : null} +
+ +
+

+ Kursdatoer +

+

+ {listing.hasPublishedDates ? listing.allDates.length : 0} +

+

+ {listing.upcomingCourseCount > 0 + ? `${listing.upcomingCourseCount} kommende` + : listing.hasPublishedDates + ? "Kun historiske / utydelige datoer" + : "Ingen kursdatoer registrert"} +

+
+
+ + {listing.vtgBeskrivelse ? ( +

+ {listing.vtgBeskrivelse} +

+ ) : ( +

+ Ingen kort kursbeskrivelse er publisert ennå, men du kan fortsatt gå videre + til klubbside eller innmelding dersom lenke finnes. +

+ )} + + {listing.allDates.length > 1 ? ( +
+ {listing.allDates.slice(0, 6).map((courseDate, index) => ( + + {courseDate.raw} + + ))} +
+ ) : null} +
+ +
+ + Se klubbside + + {listing.vtgLenke ? ( + + Ta VTG her + + ) : null} +

+ Sist oppdatert: {formatDate(listing.vtgUpdatedAt)} +

+
+
+
+ )) + ) : ( +
+

+ Ingen treff +

+

+ Ingen VTG-tilbud matcher filtrene dine +

+

+ Prøv et større område, fjern kravet om publiserte datoer eller bytt til en bredere + prisgruppe. Noen klubber publiserer bare pris og lenke. +

+
+ )} +
+
+
+ ); +} diff --git a/frontend/src/app/vtg/page.tsx b/frontend/src/app/vtg/page.tsx new file mode 100755 index 0000000..a29dac3 --- /dev/null +++ b/frontend/src/app/vtg/page.tsx @@ -0,0 +1,32 @@ +import { API_URL } from "@/config/constants"; +import VtgExplorer from "./VtgExplorer"; +import type { FacilityRecord } from "@/app/facilityData"; + +export const dynamic = "force-dynamic"; + +export default async function VtgPage() { + let facilities: FacilityRecord[] = []; + + try { + const res = await fetch(`${API_URL}/facilities`, { + next: { revalidate: 0 }, + cache: "no-store", + }); + + if (!res.ok) { + throw new Error(`API returnerte status ${res.status}`); + } + + const data = await res.json(); + facilities = Array.isArray(data) ? data : []; + } catch (error) { + console.error("Kunne ikke hente VTG-data:", error); + facilities = []; + } + + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index f30b4a0..5bae6c9 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -72,6 +72,7 @@ export default function Header() { { href: "/", label: "Hjem" }, { href: "/golfbaner", label: "Golfbaner" }, { href: "/medlemskap", label: "Medlemskap" }, + { href: "/vtg", label: "VTG" }, ]; return (