From 918399bf62332bd1b6c3b2f76e6167aeb45882c9 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 12 Mar 2026 15:16:41 +0800 Subject: [PATCH] Fix Gambar --- package.json | 4 +- public/logo-desa-plus.png | Bin 0 -> 12289 bytes src/components/sidebar.tsx | 31 +-- src/index.ts | 381 ++++++++++++++++++++----------------- src/vite.ts | 1 + 5 files changed, 211 insertions(+), 206 deletions(-) create mode 100644 public/logo-desa-plus.png diff --git a/package.json b/package.json index fbe9b89..b835530 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "scripts": { - "dev": "bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts", + "dev": "lsof -ti:3000 | xargs kill -9 2>/dev/null || true; bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts", "lint": "biome check .", "check": "biome check --write .", "format": "biome format --write .", @@ -12,7 +12,7 @@ "test": "bun test __tests__/api", "test:ui": "bun test --ui __tests__/api", "test:e2e": "bun run build && playwright test", - "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*'", + "build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*' && cp -r public/* dist/ 2>/dev/null || true", "start": "NODE_ENV=production bun src/index.ts", "seed": "bun prisma/seed.ts" }, diff --git a/public/logo-desa-plus.png b/public/logo-desa-plus.png new file mode 100644 index 0000000000000000000000000000000000000000..78f4e84340579a330a2f3ac36d68ed61d8e0ad4d GIT binary patch literal 12289 zcmV+cF#gYpP)613;g(M^))X*f-q=*U%igoR* zg%x`*>#jccefsTtZ`WPx)3vW_Z>+oOqJW}=-i#nk0tpErg!Dd{e((FAduNi#WF~WG zG6})ur$c7$J@@o~Kj&QF6iz5OoWj6^3*%1zp~Z>Csk!y<0~NxA!b!rC@xc-($V2RX zdF)wtz~w3cCw2sWRz$z%vj`Xc+X0TNg)yoWM-G)CH?9`N#SX8^v5$Zu4UYa3{q>^f zHUi%~8PT5x^?o*gQPzlww~czFt-@S`2!Z28-jHF?ezCgf5xfQ``lzrvcy^-yCg&F3 z4Tob)c%9)VMm=9?5HJCYaHyrkVpzZhy9NcoueV2FVG6bia*mtFVRs_P(p9b}Q zHg#Th-OP)|-_Y#pP>Ye8Zv>Mn8c}hEjUT?Y^0Q!1_nA>Z%)5CbT+Ue#2YG~9d1})3 zc%lhooCs^jRva(+|JeWSDtC$@Of3Dt&raGXhSc&0YJc^bP3*W?bSX#-g`l})h%s(_ zI_~W`kmrqGjl@eAJ4r@>qi%4Kw&k#iXsHunv&=;Hq?Nefw?{DN>dPTFlr=_ylpIP3 zB1Xj!(+^2-;iQpTzWe>3UN9xcFvYb`)mq|?g3Lz~(nZA6$7m8EIQ-|5{*vKH8^Tp6OZkQRh50eO#czhnU_T0qay! z!pLxO*_>#v8;7*{wHU1*jgn2(klX9lPJ5IS&}bV1@T#+4c#pZbWd9%_A`E zv|RAcOt^$(lD0HWSd2?>LF6s;WD``{YSfTx-PKkGqi_TzCGB4M4TKXUSbO|IVt!ve zTvRji{$by%M1H#9QzeZ7XsYC4Y2ze9sV}2Pn9$LXv}gv(5312t&q*e2QjQ?~b^Dw+ zi`j;(`DwS&_j}-H5{Az;PLy{m6cDLEB;g=y{*wy6un@x&&42=y(Dk$Ccgp>S%3Ee2l ziD<`Vx5C_70VB5#$B+ICj-%_nh=%iOQMiyNU|vBsjA=7rvv&kZt$|Trk6qtvf}4?@ z|K{v#CS5=JgVx&i9zC0!A>j2}ji~;IMwTF25qyFuO;6E6$yzyCX`|-e@|_u4gDClb zq#kgJNX<-WdE)97Cd$-j>o?JbhiK*GA zW6Z2i(9zK2@(J_2@-#F~+e<_wclx=@$!YEEzn4%217Ja6tlmaksx z+oGR0k;vewr(8rRYGj{z09grXShdpLD@N*c5*Kauo-h(ln4FY}q{+`6w zLhGXZ>6y?Hfwb2{K^4h&YvGYF?pH)4Pq8w0zq{MK@P%(IN;4z$j<$Q#0!^uo3E`2GiX2?8V!80~1kYeiLecJtT`Ll5;(LG`&Eyp>`y55eZ zFL&aGKUi??y>_6Msab4shzmFW-i&LWw&Iu1SS0?_D{dlkf+;0x;lzhXZS`r}08bRo z_oqwldp`JwMbx?A%uq4A6h$@Eyc1Q&r{jzzcR*IR!d^Ug0w5E}q{YLKJNY=wH94?b z*iiEkCJuj)^S8FS&{3a`%yHikp)N#dQbZmqQ|2-_EZx6@LF5Ip&+?If4)Z=qlCu6a z=QfSN*9(Hsm4}KwORhLP}A$^8sTEkxKU%d*UHC;g7 z%n8sLvk{uNhg?=I6>D3J6T1=+1fwB){pL0lz!ekHTHF8At~b`#HQa3y>5hTGq7fdJ3+`{Zf@i; zV)h59*mXW4;QsA%#KW>|IsdtgoV*e3wQAW%b3)Wb3O1N%);0b@QR?ot%YB?a@*Z?Z z_IGG}g*?`XPQ8wN+Yb_nNK^iBQSB?U??`&E$!zb@U93232Zyv#F5I!LL%PT4rMV_} zcR4win5s&ga_)c4&HDPW^;Mqhd{<*44ZnFxW?nRuqNvD;BiqYS{MqwZ`*J?&O3#8Z zu9DoFBoPD`!UCx#E|);wl|>jYEngYf_()imf3JrG6kd57RH|__VcaTK5JY@Ti7>t@FOiVmGGrUr_ui^d2GeorcYX>BDA2&Xq5^b4#gKFzokMxq8%= zJ}QcnsB|)KRx%D8(QbREnNM2~dz1GQyt@*SM`ZJqH^%H*QG~Cb8&3ZGml2hqB9+YB zsTWFayyX5!9`zhf1~7@5GW?H{_f4q;jr5o@o@7c9aBSCIkjEnwCMCcUH=o=~@shpi z4MoIEjLk_>L7mb=1KIx3u9xG|V*=P1@;X@dk=Us+roovTEy$jdc9Z)z-}TSnpkJ)b zo#W!+!zfXn(zM45O}KEs<1F3L}*n~$W?85rtlPh3JqdK^tKMN*IJ`UkRRo%j4MohjF-=>ARj-iPBwrKz!zXa9HKvz1Ii?hm#_Hv8Hn|dUTBEsrs$Q-$_)P z5S?s9bW&tKv1!qWQ5!Qa-?C^C+*-yU+9VNSZ64c8jc;FhA7|fgfFbU6GVEM5fkN|C zam1Z$zKK zv>Tw!C3~pkEmn_hFO;^WpS^RLqun`{2?77lA;j_orf8%<$9K`j$r?;#dbt|kz5E8w zy!mWs4A;P6lUmpJV?I6Rv{6Ji=?41tIGnCfx{`cLE>Sd+BvX^pI}m0K8zYB~*>5UD zn+l|0p;8gRTsuaSgZ!P>qG9`&sHm`e?%;|mcx>6CMbz+k*sbG{HfA2w`d<(W9VK~@ z>=sd)klpsGi;Q&&R@U9nl}Bh0k&<{NoNa1%Y($~r1D}$Jsj<}rNjXg2d&_-M{n@2O ze?>&#mzvmF4)&9TNc=WHHeipfU5dO5&xg|*8@&I0UD7nW_0hwTIxz(rl?5SGhzP`j z${!Y@rG~=1oW^e=^h17fS_52`oyeV-PYfU`By}|^oQeS?<_{2i44Mo)YH*iUZF^t@ibMfw;{&56$i^4eb&0aT{G*7>73IVS{7P?DWDn8zhGXCeiwM2TRA z8gvG`wCFGt*ho4ijo*aRE}aCQKr{NDtv$VnKd$&EieC96!rKEPBR=zb$@UAAO1R!N zq!<@P?S~gmK?p?4^&`#DCz3$j!1<)8FaKQHb{1-OY`IHwM+Vy4hCvm=IC4_-dEhr{ zHP<#OA^slV1Sd_*G^FP3!n|9`G3UBxFnan~$X`Tun_q(QZMcV6JP3zCL=>Do4K_!n zwCpxa^nQ@E1#=TywRpSxg0e_UKhm7MOXYoAq|@#sj_Y^1Mo`6-6GBoZTomc; ztcyW>o*q&5!0zX8v}UJXg__bvu!-7IRs;1^XyaX-;-79{F-2;24=Xd1bqL@ik!c|1{ccFBp0|Tn> zb3dE?BpjL!wjpB@8t0 zpP(OJNiJFDYKle3U@-}|K%i(n{nAlo3lNo908_@z0EHLEopS^&$JUVT@daAyH>0|^ zq5ENY+8+8O%+J>$Cix15(jxdiBgC?{wRah+K+t|G)OK^7+9g*{mpwlN5CGk zbKM4aOZBrb#Ie*pY=g*DmjaKBU|1ZZ>9~se@pOun{4YkGQG>H@`xnNXGYT?+NNWZa zWGIh8Ldt!T13}T4h#aRKuwM{ z&>v+gN$(D_LnYWEtU339==)7MBjB-T_2&>A%y#fBlOGsxZoSA(an-gOw&QUza;KDF z!Mz`n{?DS%j97ws5k%xpPvwlUb0klc60i-{Ew&=GVktC=2Z4vPMS}$G)d&Yc_cwSK z^z(p-lM&QxavgB0+AuUB*S7umIiwP~m$WLNt$q_c5e!?;)mA6K-f=ZXjBCSb zcQcnVGnCbRlu8DB(lw%`LrHwb4bobT&HqBx)@_JNW2%=j?&=^37&H7Xud*Wqu}5Y< z7a}2b%XuyYJ3k*U4ld9nx`|$r%|G`d8koVu1 z>ov$w@4}p4u?<&D?F%eX;)dN0r+dY3@FE06$CIkpvI_P4I$&wrMblF08Z~mmM;9Qg za0DU@@9qmgBw-Te0IDl}K-QDfM_nf$FyM1P|gd|?l;}t9uiO`Vn(*MnkY(oDXvu6zlFGpkT#-KDR3i1Y2Dt?ec5!7d@Avu%0zXA%OH@m8K1q@g;D$6wCM%49^~S9zT5cC)j>SM&q#?po`j#*c1wj zYNXZeigs**y4bkhKH_1uQ1zktPC0T|Ec9Gg;{}6v;g1#r^JO@7)ryx$-^e zq&;B@ykk@!3&mQEnfJIaM0`%4tPyvaKqg;eLVVgPbhe0+ff*{;CM|Qzw?L5-n!?at zmdcJc*BUtKAp5?{2%W!aNs}?E)MyWwLg>o9sEB=kNBWFDEdv<65Gk6A5SKXvE=wvL zq>iu^0n%Altgd3W?jHVT7jhGKYQd#l0POo1o?u=|?0poucPCH)9r305%HQ<5Zawq@ zRC)gebm|r5!#sX*i5DX$M~{jMWnwG@^q)xj9pVf*ye_Xuoi|U9sL^F;Wk>8F3@Jj- z7~*(T?t5OY1|M4dL9KUE+3&(;;i2Lq%f0l*B)D3e6r}Wcxny^1$xbqUcfN@dyotvj z=keF&xkws03y3-!$vM*?Sn{CO=_GYXUQ^{ix*dEhD28HZ?>RV!zJDyG&d-lUT-^C+ zt@p_@&`VW0c?GsM`Y}ve{4hC9WNst5BZN-a+Cqr%6X82WBa+50CL;MeA`AsY5~RmO zMX3-pgWwPI?qZzCpO*>a^xZT`yL7fW6hj;q3fZ`L6k7)|Y1_u5T}G`c%d z*3tbL<-I%G*Cosmk+A0u<7jCaWJv>D6!j;pK|nzN-X1M<~Kx6&ZrW9akN?2d4Dr(N>GF354TZP#aX}Xm~tOfkEppx>U%V zX^_@dMG`$j^P->ncr)f;I}dxNYiVgk%eo!(oqe80&eSxdj=U3b>5swEBpn|CPN7aO zZ+=Sb$~z(Jo{)|&m8->$g`#r(X-HBwoZ`8}T+T7kGcami5)KwM`rhu(d>jzNJz#~T z<7~uG-~YkhyN{Dp^#BgsK)478Hh4Xg@W?}L)D3u zj|DHnZqXnr_IgA(W-_>jJL^PVGa94Lm`aj8RlexUE7-aCf{G%x@kq!Xh1le|(8o-L z)25@SuLyw?IF7BA7X3c004~HNsc>X>$f{CAC1iM+$#J3SSIxk%f-TUEdJ2^XA4lzO zGdy;Q@_p!@bQH~BS%v9WJ_bYlix6y33~4c9u>;XpUMC+-6kI(TI0nRyh$8+QKa8Cj z{7guX1{sTiBN9IabArn$sDPvK+ z6}vwF3W0R~0QK+jljB`IRITvVtt)|atc^Q=_u8oR4^dkR#H2igDYwi-ISl_n? zj!68*zR7{lbWpPr$4!vTLWjtfa=>76p=|wm-PZ!A?i1#wSj@QgD^fYQuGO=V1kb0z zAv}xXPu_OdN1pVP7B8i_kVD=eC#e_2J$5-nlaE%g0n3F@uA93t7rA=X!sgY$v3%GtpVx?9|W5l%%Kp=4ji2KYl?qGWXtG)5J|u1 zc;pGhiReTM&ecDF?H_GGe@_4C0?+k+T~Y+WVty`{=f#^cK@Xq*EZQ55G(mmiLYy3q zpl8@|sCF}(=o4T}oPhXDNYXncmpMD^kl**l6gL){*GSK@gSa_9xTny2V5Qd~#P5YM z^?zY+7v(o?4Fbfzt0g&f)Vg?#u*^Z3^lG<$V?c;xNO_0YD;gMqbT&PQ4R1e-{z{M_ z7$B|Ng$WnrkrU%qX@5uX)dli|?XgW)k`VfU$(48tyBa2W78_u-X`!=@b{~s+!r&7! z;w2k4d-7c<+s24QK1H%0qY|PEcyO3Sgdj?%r$i;Hh*LgE1j2OG?pHbwSfQaHg#Jv; zTTaF~pmg<(;;cg*Q)#^$*+MNZL#>|eoB@-M=pYNXtXJVM|<5eY{O zsQy6`!iE?T&82@v!ptM`<|?JDuU%>i;?_VLy_vo%JpDyd5mR7oIUS1QA1r2KGjLqy zWg>RJ>%T5gj3HsTM`q%Q?tet4ld8yz)sC9ovrzoi9?0!nZbuPz-zg~^eb93qUI4D1~&bK|W$^tu+MWin~f%p)s$sry3My%r(A-Jm)*Jj;ef($hJ_#Pj6){~lGvbMfs<6S03i z<5Vhlx&iZI4Lpc{MQIAK_mfvpx|uvks((sx9*v${&tli&#~z3*@wgcf&m-EzqqF5c ztbhGV=}uJQP2?>Mtwi>+qqe%&yYdbVJj!>Mz-9ZXcRZ$#DzKP|A>$VG6YB7%H>ZWX z0dsJ`3`SiPi}Z3t+5&h@1zH-Hqv_yl*z@IKcPfu@9y)QzOXmkICKep59&Qulz{Z30 zxs07>h4|FlkUDZ1T+U2sUkF=yE4yI1fKxAs5-#2LP9ygLF`z_t_SPm;?6}f>P=lN* zK7FaQTu5nOF(-;zvl}Jr%Y)a(e-6^Wt;hd@xbzpLbHRGGAi!f=I#yH=Bfb+%f;@5*MHbmCXd$mi73xcWf>YRx=Bmv&w)+^my&awxU4}0?c-}=i zuak(+j6oKSexXvwlE>70wIOlSZFMNe6ZHKFHKiv+5PbA^8O&;^+@u;d&AueuUP876SIq3Bw zit{M{<-U{Ov)wIHt|ghs^7FMl+oRCqI1p%U=XI?zz$hNdFfD5`9s0URpLnvFjioSK>JWUBdzjt%@?Ww-<6c94e?ned3xv5*NZbFjlSy7 zVZfC~8fBSyt`gkr7R3WBU4CMzB`yy}JD5^^~)LJccTr>8p+m7y67&|aw{sc5O zm!Z1Y5}*hU!>8rIWwWE|$7-)MflnSBHfa>b%={C+edQ92n3{#o4j#vUI)d=QVc5iB z(5h4@-&xtCo;fpez;h#{_Ni26G&K~vj}wi^R^T4UojDC^eKK5*R&WjrIBgO%dKHXK z-}tPl>KBGUq=ZA}s0^s|S{&J30q^Rhj~x!3jzh(+3Mm~P=P>DqYtg>^;bg}cL@1y4Z15vRt`1|k1Abg0Jdif`? zG$j&|jP5auCNB64*7mWmnGZu36^jXJ8K|1@5H`R6G$MU0QP%kXfJVy>Ixs(AWh zOBHIo%G9Gcgg)RBpEMzLd3MoE_Gkwy4%?W&v$Rp zZ|uHS*j;Y#x)3)Vy#$LMEJ1Iu^j`f~=-p;-)xWiu6&)JJbdcV zqhEbrqo=69;j3m&TKo8dp#@X*|V?pK4tRAnMcXO% zmVlzymx4=s1kou!V9xwe;5BC>I&l`}-<5$iFW!L3i|-ZF@*hJ-lNt$WZ{dqS-4ArF zm=WMiy_f!F9r7g3xcO#8>HY#i-HEa-3sJFcE6%(BIau4Sf~z0~PVpgZefJe)&P>4Q z)7L_y8V{@Ga}>RL8QS!>A!%xiSW)q3p9VVM6jDV``wpzx1GR7mseG@|?|)KWdW@gB z5$5I$j8AGo`Q~XT+0>3RuD$@#$)8YrR5-eC3HGi26lW}X7*Poip}j#1!;F`(_Aig3 zdjfTgiZax#cb-ss9reiw@2)Z(_^)_oLwa$w(ab zIQ6Xq`l!p%RQU|HFMkkc%~Qjf^C@&O7sD8}7sr|k(Y9{^jnDPa8WRwkybvy7E53eq zHkD6ozi&k)5_#F{ej#s3uG%v+wP`FJH9W1>kt zdJt>hd=0tNt|0aIVyXj42W}cV8<%3^d+*SsH6g!XEvHgX5^eSsD0=xKwCHX|%8X=G zRQ!*3Z*vZtS=7g*VeXAT!e;##Up{v$JgRMeJ~70LH^6Sq#&~@@_I^AaRaH$m{mLg1 z6BCEH>}5EzF&jj?MTJr?f|LrM+DJLRj;K-`SvBUo) z!pV`o8)7a5r_mvA@7J!&4@y^zz{Ypt(aGI{tZ8%L@LGJS*3N_`Y6_AIqLDi6HSF0m45(Z{qFmlA zRJCU%*0p7#sxk>_qh6NMr6qh0m$enEo>Rfn`hlE}aR$|AuA=r?&Fc`86fb?BGCjzE`@QG;#%w?wW(L?@uE-tCy~|)D>gr*CVj)LlaEN4;mfCvQ$f*}92fjG^_&J{1c$%>B`SB! zMndk}V4OQC@gEQbo4ERU9_{t=`BysK=|s$D_G+xd(twoQi-<5MVvXr`bWdmj*zTn7r`Y~Nmm4y1s%2R$e;TN8P}_@Z^I@qqEwp}VeA>DQX0*fjrrG7|Edv%dCBF_=War0oq+UVMl>J$Eixt-Vb%?MVLdLtpM)pE6J5bXvFuDR zrq4v<(P~^lYGfyIO~HCLX5V-dEX^Hcx1>v|N?YA4$UeQ82hLB>QiCBZ;FV{+G zGA8XKyJj4Y9eES!V;&;Te-!nH9+c{@iA}??{cEx9Q})^k4lSyAIQynH(7C3HHTW7EQ|To&4-5 zQU;CIg!l~keY4pU@-X$vtvI}WKE8hH0_xN<>Kwgvpg@n>^YY|g`RsW(`eg!1mWdc~ z?gosVKN2I)`4mkX$70o=u7NSp3Y9AfWxKZEi~rW3>Bur=E$A)@q$NnGop z61g7zq9Vp2a(W!w-iXhCpNg6Tb20wh_c3YVW$0`=fTT>19VsHGU?Qm~c9gEPVajzC zsC6yFiYFG3S8qM_cN~oqN0Uu}hnY?|Z$3uO{t&yro`4lkUP?q{mtKn}?WtGGlaRF+ z0tXFm#G#GfQ-ABweB?%A9Gj7lpGl0-zo&{*xkz1`h#kvcru3g7@!S@v4{S~CP^o!1 z9e+Yg$>nG`ekTt9IEo0!2`zPFhuPSb2iulxHn(Ez^pTi!;g=-UCgH0;UP-EMEAiBD z7cBNelDHf;Ot@q#CaaIqY|SGPtw+iFdD!#y0WjjzN5{gDltNtVH8Mu;#<>qvi4Ibs zbV*amD^@4vp)n>y8zmbWI^zyf51z)PMH>8A_YB5U=cJFk19{~1F{O?LnLtp*siBXJ zlWT5Bf|EFDA$ijBX18PCcQ3%C9fcOn2Q;~w;IxR)}&!-MVjD?$qxMjT8#w> zs0Xjlp!B`_TT3o*LyUYvH$2EdCCmgX>2i&KSD}ZA~P~>UN;* z*sG}DbQ|_=J`9=g1Vp8L2W#0+Xl}Nlbj!;yKiZ#T10KMiK)E{{T9W{SI=b=vgSiS;@(M}o+BMy{Z zg4S<3VKnRqC$2(GX?>6O=@Zt$+^~%%*Y~7{zmJk#x1eOr4tTsi2X_=v9lk?!!UD3N zp2qi|z9K#6vi+SV)qRn#Z8SOFARFNVwAK`%<@iHrIdU8=QI(`(uBM52RLUbNp$O*2 zpRjN34`fr6L#Hi3^|5PlsOTUO#ib-oI$>_VgIaI~Du28YyT6rRMbXy&G8w_M&>|c~ z&Hme9tu03^cN|up`g?DM)aHgt@+@*6!DzaQB-Bf!di{YUTMMZ_`-n+=jry`$sjhJ; zYf)Bw7-Icua7G&paj~e}dnpbUw<9{bg!GP8s6E)TzoU&mz}2~u#<^Z9tMbQpp~*M~ zXO#=PR(wgsu!`)#g=lVh0Xsi^R+_!BiAAU{-$BkDSu;t_yNv2$Mlq!q#CJ%Oxf-oa z@1ec)_o(CR&|V(dm%s*rKjQL^4tJ|@!%mxo3xB;2Sy>|xlaYk;?>mYa_r2sNzd#Xn zZ+MO{SND;YN8tMLKEDG+?t`@t=&h>yE0;syh&tcLZ@E9X9_1=o<bYCmPFd(^Xl95b`8tHBx9J&eEzYk5Jq>XxfV zv9j{utwMAg*qY`N)RAZ}xNSEKRuys4zhLWo%#idB$oU55?C=l4U68Ynw1JMA@jdlW z^m*15;JbJFzRD2-JQFDB*w^!}uBW@p4dj^-dpr|3e;Nx4imLd;py?z?11tEX3Dz$z zpX;JmcI*f!CH?KM45yzmy{`$#zvwfVqwC@P<{1Qb^@icy8u(>9z5zrJ{H@P9g#d_H zM~y8^$EewpLVD)p7$xuEY@j(e {/* Logo */} - - - - DESA - - - + - - - - Digitalisasi Desa Transparansi Kerja - - + {/* Search */} diff --git a/src/index.ts b/src/index.ts index a8ab986..746d140 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,211 +12,240 @@ const isProduction = process.env.NODE_ENV === "production"; // Auto-seed database in production (ensure admin user exists) if (isProduction && process.env.ADMIN_EMAIL) { - try { - console.log("🌱 Running database seed in production..."); - const { runSeed } = await import("../prisma/seed.ts"); - await runSeed(); - } catch (error) { - console.error("⚠️ Production seed failed:", error); - // Don't crash the server if seed fails - } + try { + console.log("🌱 Running database seed in production..."); + const { runSeed } = await import("../prisma/seed.ts"); + await runSeed(); + } catch (error) { + console.error("⚠️ Production seed failed:", error); + // Don't crash the server if seed fails + } } const app = new Elysia().use(api); if (!isProduction) { - // Development: Use Vite middleware - const { createVite } = await import("./vite"); - const vite = await createVite(); + // Development: Use Vite middleware + const { createVite } = await import("./vite"); + const vite = await createVite(); - // Serve PWA/TWA assets in dev (root and nested path support) - const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath); + // Serve PWA/TWA assets in dev (root and nested path support) + const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath); - app.post("/__open-in-editor", ({ body }) => { - const { relativePath, lineNumber, columnNumber } = body as { - relativePath: string; - lineNumber: number; - columnNumber: number; - }; + app.post("/__open-in-editor", ({ body }) => { + const { relativePath, lineNumber, columnNumber } = body as { + relativePath: string; + lineNumber: number; + columnNumber: number; + }; - openInEditor(relativePath, { - line: lineNumber, - column: columnNumber, - editor: "antigravity", - }); + openInEditor(relativePath, { + line: lineNumber, + column: columnNumber, + editor: "antigravity", + }); - return { ok: true }; - }); + return { ok: true }; + }); - // Vite middleware for other requests - app.all("*", async ({ request }) => { - const url = new URL(request.url); - const pathname = url.pathname; + // Vite middleware for other requests + app.all("*", async ({ request }) => { + const url = new URL(request.url); + const pathname = url.pathname; - // Serve transformed index.html for root or any path that should be handled by the SPA - if ( - pathname === "/" || - (!pathname.includes(".") && - !pathname.startsWith("/@") && - !pathname.startsWith("/inspector") && - !pathname.startsWith("/__open-stack-frame-in-editor") && - !pathname.startsWith("/api")) - ) { - try { - const htmlPath = path.resolve("src/index.html"); - let html = fs.readFileSync(htmlPath, "utf-8"); - html = await vite.transformIndexHtml(pathname, html); + // Serve transformed index.html for root or any path that should be handled by the SPA + if ( + pathname === "/" || + (!pathname.includes(".") && + !pathname.startsWith("/@") && + !pathname.startsWith("/inspector") && + !pathname.startsWith("/__open-stack-frame-in-editor") && + !pathname.startsWith("/api")) + ) { + try { + const htmlPath = path.resolve("src/index.html"); + let html = fs.readFileSync(htmlPath, "utf-8"); + html = await vite.transformIndexHtml(pathname, html); - return new Response(html, { - headers: { "Content-Type": "text/html" }, - }); - } catch (e) { - console.error(e); - } - } + return new Response(html, { + headers: { "Content-Type": "text/html" }, + }); + } catch (e) { + console.error(e); + } + } - return new Promise((resolve) => { - // Use a Proxy to mock Node.js req because Bun's Request is read-only - const req = new Proxy(request, { - get(target, prop) { - if (prop === "url") return pathname + url.search; - if (prop === "method") return request.method; - if (prop === "headers") - return Object.fromEntries(request.headers as any); - return (target as any)[prop]; - }, - }) as any; + return new Promise((resolve) => { + // Use a Proxy to mock Node.js req because Bun's Request is read-only + const req = new Proxy(request, { + get(target, prop) { + if (prop === "url") return pathname + url.search; + if (prop === "method") return request.method; + if (prop === "headers") + return Object.fromEntries(request.headers as any); + return (target as any)[prop]; + }, + }) as any; - const res = { - statusCode: 200, - setHeader(name: string, value: string) { - this.headers[name.toLowerCase()] = value; - }, - getHeader(name: string) { - return this.headers[name.toLowerCase()]; - }, - headers: {} as Record, - end(data: any) { - // Handle potential Buffer or string data from Vite - let body = data; - if (data instanceof Uint8Array) { - body = data; - } else if (typeof data === "string") { - body = data; - } else if (data) { - body = String(data); - } + const res = { + statusCode: 200, + setHeader(name: string, value: string) { + this.headers[name.toLowerCase()] = value; + }, + getHeader(name: string) { + return this.headers[name.toLowerCase()]; + }, + writeHead(code: number, headers: Record) { + this.statusCode = code; + Object.assign(this.headers, headers); + }, + write(chunk: any, callback?: () => void) { + // Collect chunks for streaming responses + if (!this._chunks) this._chunks = []; + this._chunks.push(chunk); + if (callback) callback(); + return true; // Indicate we can accept more data + }, + headers: {} as Record, + end(data: any) { + // Handle potential Buffer or string data from Vite + let body = data; + // If we have collected chunks from write() calls, combine them + if (this._chunks && this._chunks.length > 0) { + body = Buffer.concat(this._chunks); + } + if (data instanceof Uint8Array) { + body = data; + } else if (typeof data === "string") { + body = data; + } else if (data) { + body = String(data); + } - resolve( - new Response(body || "", { - status: this.statusCode, - headers: this.headers, - }), - ); - }, - // Minimal event emitter mock - once() { - return this; - }, - on() { - return this; - }, - emit() { - return this; - }, - removeListener() { - return this; - }, - } as any; + resolve( + new Response(body || "", { + status: this.statusCode, + headers: this.headers, + }), + ); + }, + // Minimal event emitter mock + once() { + return this; + }, + on() { + return this; + }, + emit() { + return this; + }, + removeListener() { + return this; + }, + } as any; - vite.middlewares(req, res, (err: any) => { - if (err) { - console.error("Vite middleware error:", err); - resolve(new Response(err.stack || err.toString(), { status: 500 })); - return; - } - // If Vite doesn't handle it, return 404 - resolve(new Response("Not Found", { status: 404 })); - }); - }); - }); + vite.middlewares(req, res, (err: any) => { + if (err) { + console.error("Vite middleware error:", err); + resolve(new Response(err.stack || err.toString(), { status: 500 })); + return; + } + // If Vite doesn't handle it, return 404 + resolve(new Response("Not Found", { status: 404 })); + }); + }); + }); } else { - // Production: Final catch-all for static files and SPA fallback - app.all("*", async ({ request }) => { - const url = new URL(request.url); - const pathname = url.pathname; + // Production: Final catch-all for static files and SPA fallback + app.all("*", async ({ request }) => { + const url = new URL(request.url); + const pathname = url.pathname; - // 1. Try exact match in dist - let filePath = path.join( - "dist", - pathname === "/" ? "index.html" : pathname, - ); + // 1. Try exact match in dist + let filePath = path.join( + "dist", + pathname === "/" ? "index.html" : pathname, + ); - // 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build) - if (isProduction) { - const srcPath = path.join("src", pathname); - if (fs.existsSync(srcPath)) { - filePath = srcPath; - } - } + // 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build) + if (isProduction) { + const srcPath = path.join("src", pathname); + if (fs.existsSync(srcPath)) { + filePath = srcPath; + } + // Check public folder for static assets + const publicPath = path.join("public", pathname); + if (fs.existsSync(publicPath)) { + filePath = publicPath; + } + } - // 2. If not found and looks like an asset (has extension), try root of dist or src - if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { - if (pathname.includes(".") && !pathname.endsWith("/")) { - const filename = path.basename(pathname); + // 2. If not found and looks like an asset (has extension), try root of dist or src + if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { + if (pathname.includes(".") && !pathname.endsWith("/")) { + const filename = path.basename(pathname); - // Try root of dist - const fallbackDistPath = path.join("dist", filename); - if ( - fs.existsSync(fallbackDistPath) && - fs.statSync(fallbackDistPath).isFile() - ) { - filePath = fallbackDistPath; - } - // Special handling for PWA files in src - else if (pathname.includes("assetlinks.json")) { - const srcFilename = pathname.includes("assetlinks.json") - ? ".well-known/assetlinks.json" - : filename; - const fallbackSrcPath = path.join("src", srcFilename); - if ( - fs.existsSync(fallbackSrcPath) && - fs.statSync(fallbackSrcPath).isFile() - ) { - filePath = fallbackSrcPath; - } - } - } - } + // Try root of dist + const fallbackDistPath = path.join("dist", filename); + if ( + fs.existsSync(fallbackDistPath) && + fs.statSync(fallbackDistPath).isFile() + ) { + filePath = fallbackDistPath; + } + // Try public folder + else { + const fallbackPublicPath = path.join("public", filename); + if ( + fs.existsSync(fallbackPublicPath) && + fs.statSync(fallbackPublicPath).isFile() + ) { + filePath = fallbackPublicPath; + } + } + // Special handling for PWA files in src + if (pathname.includes("assetlinks.json")) { + const srcFilename = pathname.includes("assetlinks.json") + ? ".well-known/assetlinks.json" + : filename; + const fallbackSrcPath = path.join("src", srcFilename); + if ( + fs.existsSync(fallbackSrcPath) && + fs.statSync(fallbackSrcPath).isFile() + ) { + filePath = fallbackSrcPath; + } + } + } + } - if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { - const file = Bun.file(filePath); - return new Response(file, { - headers: { - Vary: "Accept-Encoding", - }, - }); - } + if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { + const file = Bun.file(filePath); + return new Response(file, { + headers: { + Vary: "Accept-Encoding", + }, + }); + } - // 3. SPA Fallback: Serve index.html - const indexHtml = path.join("dist", "index.html"); - if (fs.existsSync(indexHtml)) { - return new Response(Bun.file(indexHtml), { - headers: { - Vary: "Accept-Encoding", - }, - }); - } + // 3. SPA Fallback: Serve index.html + const indexHtml = path.join("dist", "index.html"); + if (fs.existsSync(indexHtml)) { + return new Response(Bun.file(indexHtml), { + headers: { + Vary: "Accept-Encoding", + }, + }); + } - return new Response("Not Found", { status: 404 }); - }); + return new Response("Not Found", { status: 404 }); + }); } app.listen(PORT); console.log( - `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`, + `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`, ); export type ApiApp = typeof app; - diff --git a/src/vite.ts b/src/vite.ts index e8ccfab..5af027a 100644 --- a/src/vite.ts +++ b/src/vite.ts @@ -8,6 +8,7 @@ import { createServer as createViteServer } from "vite"; export async function createVite() { return createViteServer({ root: process.cwd(), + publicDir: "public", resolve: { alias: { "@": path.resolve(process.cwd(), "./src"),