N|-Sx;`}$6GM};UbCbC8TMM
zvsGNal8+!eKMZ2?U7))rj%w1R#>%)LUa#hrUsZ7z>oPa_p{hrFX)c_1U4tG`sp^tw
z99&%t`;E5{B-#t}bq&329QF{IuFr<;o-@#29|I@xY9^w=N>^Fz)pAQdG}i=?pyt4ET^6ji
zR4{Qh`za4cx0K<;&N?FDWE|WON1q@1-by<2>h1PtTX|ym-#A${I`uCXv+o&Oi>2MP
z-%|t+$xCn)y?|poO6fZ;fz9Si@DRHX@7*M#Y9nY4`2}Y!2av8jiZ}%>OQ0Ju(yx&y
z*N1GaQMS_Ra?l5~M}K4?f%b&YXbR`{6PQBviND~i#YYsGOyHu|M-*E0quiknO+gdz
zmT953Qb2=l1~gVA!gljj8t{{8;6IP-gCoc}{04SgFXPz8dX|Nvu`)K%Nv?($SLKyo
zXE7AX7tvpxS75mIG#s~e;_wfpFkD+i4Z9saJKy5yh8D76#V}f13EgE}icA%Ze>j8v
zt21D=qlC@)ANV02$9Ggwr)-AR_97hGkcI;r5@GTaS^OUpm{3}7D}d?dEVxQufF+5s
zt>_t;Z_b0owp(gPexdg#`AHifnd@1ICGe&H1Gq?m<}UFX%I=WLZC!rlflyo-=jmFUA{|Rjo6S$fD8SU|(
z(Gu|)&0)Xbf;W-t@vkU3LXSs(#s&AUIDPN~&O3fWD+zXx%1s)m^I`ZyHV%JZi4&V|
zLw7|stVvL7oIau0b`b7jH|h1Pwg^SuT~>MJH&Rp=Cy4k?Z(M`3~z)2K$)UrHRN6AX)t&M}xk7;n&T?^w4r=Ynygv2!q
zUecFgur3kiTe7f!eH8o^T41&{okTYd2i7N$Ko`POrU3!+?Qj++TH3~mb2n<1&eJ6MLWfDnID2O?X?8blYllXmSQmDF1`|t6uNjm~gZq!)Dj1
zI~MePSZ*#LN^!V@
zoMA+2u_X^4(nOgXGf5b0;iuS4RGI^4i5eKJkH-lyqSPHZ@Y&k{lT8`07cIewJykfV
zc7su^?apEx-jqcIb()c}&CYVTN;JV$tOfQv>TrDLdANwS&}TP5XDt`MO@WjA+2)Sw
zZY7>*{`+caSeL8G#<=Ilcb>-a-6brx>L$?wf7vb~$2{2Ys)ZwcudZU3ad;gKv^$y*
zq1=lIsUcL^lEn|6LZ1EzQkBM#sxXWMxjw{6_aaa411>mC5upy@R_a%DBut|%mfNu9
zD=zwcMfC|1R`bs&F#JRU`vrA=M8GDasQ3PWQ-*J8u)YAJP093~o`S)O3fOMBf+IiH
z;H2!k$qfBBLHRn9ybu7d{Pv6f%G{una{ZHjqVM3a?K;fY*TQaV3yy8R058c~FxhYh
z2iK*+jI8~!?S&+u`Sd&!hCjwrhpnK;M7T+vN3c>m9nZ#bu_8KthU|ScTqLXEuUwC#
zJ9FV7bAdW^Cj8_ZVX`@$Xtj*aD`V+e9JzAD>MM5@{&LsgE!z&;9W_K*<#3UzLzwD4
zmLF^UV+I$R=(dzh>*#qk$O{$x8+Bsr^S@LicN~q>ZmzQ1k$2BxOAZXzXTx2h6;9%f
z@Q`eQuk1BAN>tJJl@I$p6*RaJ#cr!W@ZKlz6@QK}i9wXwki`%Dj7*}|Or=RA$n>$A
zrZ9#a-4S+k!H%fUxSq_#TR-DU6p?GdN1XHeMB+-sYWf*@2S4Jh`4`kUf5171Pq-EL
zugEfd!4{oZkhmMJ%Z0DZ6BeQ}`=KgdN2ErC*CTo5cU7FW4T+qTdtcxw`Vcl-8sRS1
z1(!XYj4+PxK8FMAl8GwoVYR)O1Tq&EM5vAuWw0d?^;Nh8N3m+SOPz!9rbH&9CnV0m
zVmk?`LL;1{N@2IB2v$4u>3yf*y_e`$>=aIjmcxlUxWB>`mLuyS(+FqD^K|Syf|Rep
zQ??l{;!W_A>x8p-13hnqx6Cyd(BERPE&&I=Pk5W=aXECTcanFjnZMN+w+1)(X_r@-
z{gi|gyGm(ryNnQ(M|6#EP;G~oTr)ydZX;6jK927pXR$pW`s?H9JGp{rjb}u)*AS&N
zh!nL^T=e{idjAhZt;2{E?M4QPY|7pdB*_mU-(Vb9LZ)#e@eA6MCU7nOE1FM!!X^K|
zpvr-)ztt4-4}PNh1;s}`q4?-9%8yN=$>(R}m=2QbDIf=Q7H;D0u-ks6&286hUR;$|
ze&?YAA_uKiNj)|{U4fhEb)wg59Q+{*MjLWS46ETof@dR^LjqUd0B}Az=+uX@i4AF|2pzljs)0iRjjg
z&h?PKM4wv=f29_Ls9q<5y$%-=bPu^Y7LRolyNCe!E_(lCgztL@XNfxcyHa4aC$H;5
z)-#how5ZtZ?j0A&a&i)lNIBS#VC4sN%{$2z+(CqP7Y$N%aFed5L8^_#
z!~+ytV7-&RAE^uQl)i#6h1Up?=|PU(6zY9GW$
zXbzepVx7jVl)sR;{){V;KeO!x&stBT(s~L-#*@f7Fo8-U)-DU<%HUFN)A$18uRa$-lTx$Tbn9(VB$SZ%Gw@ttJRcjhtLwAh&e7ikhr(E^xn
z&W7>UIJipHAW-QtJY;L&qi}%;H49d|v*9CON4CBKmOIjkL@%@m;m>+}nsCrRzk-mtnW-9Erv|Bxt`!f^IMT
zWFNBZ1e+bD_k1-jo$IbgqX5~PY$DBJPhD5B&zpdezA3)nyQp3)xS{W(T2}8Ue!A0Lt^y~uy6Bp|
zAYpxp812`H*!L3Any(O|b{C#<%|x*`i1=?IT>S>z_SO)s()U1O9HMp&o-&u|x?Uz{
z(uEYQ5tjJRS^bKm)5uW%fJB*oB+3pTokTW$-w-bQeMEiW09*3f8a0g$I=3l=6Vkt+
z!fqOQhF_3pFom4`pV1oj7Ze(g;(E-#(rd$Q8RpM8caCgi
z6A5btcfTw|s*~`^H<10mKpnM=I&dw#h+N%>YLAQO(uG5AyoM~0#xe}ta1&R=8uSU8%PLlQHO71L>r*eMr2lxP{k)m
zJw)`X^B(b9eTY#VMxy2b;&flaTka}}NEb4U`U^V?#`TBaPyg;j_Vw+tb*abN)10Nw
zcDT@W3{~lXi{vHt|A(qRK$O-~q#F&;HGhjlonE@0w-KaD!m4(gxr0c}E_f@}(?Hlj
z-x=pD&e4EbN!PfUg%aXaxXoCm&>sH@S^GwjC`Z><<{P!9DU2iEU<{p!A8|YFXS794
z;a2+3XpR1gOM$=OywhJ$ZTAJGmYlGTB2#A!7d$6Xe0chPliw#^T$NXN<=-lPa!qnR
z@(n#fO3g&8NhGkRVY54rMDRQUl^ftBUWz3BTVy%QsFqOYt-;Y-?nrjT`T0vU#VNINuu6vG}8m?wzUdxY~rBVKK#Z}$BjM3viU
zJj0p${*12luehG{Gdk$J%RxV*C4i{a{xfP%d_?Ynzal|-5NFLlOkQ;R
z%-af(S9s;$6_1rDGG9l4w8IIbY$XY4H4$hVLNy!Mv1pA>oRBz89k`x^wiw}B
z&FmaknG)EEXORfrN4owK1S+(^Pw^t+^@&=Qn~9_@z(ejl32+zL+zxokUm)vRPn67A
z+XiM~{S`aO`aVXHEp>MNaikC-rBTf@oj{h!AYyf&QhiRs{0uRA50Gm7xFA^PLREA5
z-QVo3X0Da=YWb>G*83?};iP&yBDFecKx=}xLIWbTJBik>Bh$Eti2fBa=^7**c#Zh|
z-N-Q;M4a9W_{d*@A6@H{tE^d6FTCET7y30vhTm5(*7$7jK5_H
zLhJtQ7@N(A?q
zKKCAy44=SeNA|t5L7iUxJ)^&wUAJx&4{8dBkfyL+ZhINIB4lLc>pJ3iyJn(Vvm2@&Q>?(-p>%sxXEOm2tF%eMU#jXBH0V
zNce*53IB?gkpGEhzptpWpGJ}C&u!($K5ygo5?tazv$qCEb|%7nM*^Ir3K2?{G;Cip3FUQ0xBg0Xh}5}CcAlt8
zyOmzMf|P@gNeEsbl%B`x+@WLFkYWB92}Grdy04LAI*hpeFOhv{0I_O)$TAv7n(;g2
zS`3j8KSP?~TN2erM6OQ|O=25O!t5k=mc+cGwKVv?*YjKb8-A^#TAzFWP=e9b!Wga2
znsk#}h^0X$PWuMjaQW;WN5Mk5F`c5NRgeH1NEk|Mv+p
z4)+k1J}1F_LD#nf*~YJsV)y|5>gN%uOV{|oJ%p&X(sjH|M0*=~hewcaJc_2UDO_})
z!YS2BCaxJuACR~26G~0Kp!MVw?xg*UdpTTa;1_fz{(^I!Q)u@6OHYZ-&%C%Qukgx$
zXYp66F?WkDq{5BE&{(`mN%@zjcjl$S?SjBgeMtJh!jQ>!JxqyfeF0TF!*VszWtwaGSl
zie%$kNH*$X0}^+Q@-2H2yZ;^vtOt;5)r&&AVH#B4Aj_u!3=o)e%fz(6yiC|mc
ztyoI~&UM7jEIPx_<;ncnv4abYzh9qg7SGG0AAshzhCi?uW$-iz0%_(TL4EQR8GVqHLoH>
zy`HG_D(oe55w3QH#Fd0X>l)GL6Qmt@h#=(#66F>mu)B!gPn2eG4e6$L$O1n=010&N
zv8P0(kC0+?AE!xBGmLsrU^Rp?r%@Cf`G8`ZPbjgS###Gexec$q6)@c#54&A?u-lWB1G@KUHCLglh5E+9s;6G=psN&D|2LH`C4xa(qkpM>*1(hfdE
zmI+-ygXajR!7Ib;ISKAF`v2c^*%FA-d`QImgs$~{oHBcfaE&(Pm_McW--DC%S-Q?Q
zk!*0A1|crwatEmfeROSyQ1AW)o$H7}0vkR}wi@BUtqk
z(n%n=i7{WLYD8*Zq0Zh#V)=rJNwUFRqOvNlhktyks%fOw(7$H76RgeuJ~e-;v1NM20C@U$Ym8)@&!yK93;P
z^YB%yftOq*0u<_zr1cD0hn^QkX|>g)**C@4r#~^fd9hpO+0DKUAI2vCOeQG`5hUQv6&Is4Mj5r-G4ecDlROlM$-$A4X4LJ58b1a|&g4
zUvSQeNbC47$g>zm_K~;9HYZDL{t}soU*nAJ01`>4i>>;QbnrT|4nJVR606mTOrkh0
zmKmbj1YeaZL};}jN%s-`t}6)LcL{!q=iseS2`{BmBFgg1QTk0~;Rff63q89+tAk#6
zRmVI$(U|tqq9*pS-Gzi_HWw3LST&{gSQPu-52*Be<(FX6mK&|zQI%?V|4bo?VW!y~
zoH_msr!0vkEgm39tq$QTtwi>XNYd{jF{SHZ&`HF3i>}diqW%tqX&zq6+j@LSsFKKj2C9-!YFs5jZN^CwjL>}zM5s5AZS;hQ
zwTrASQR|_bD71cwY|DEnuzXEoL&wb?lQ`ZbI(vtV!!J?dIEs=JA5i7+7ZTPlR6ioe
zWR$3Fg2ZYNnoy^fP^N=u!E@YD&qAz5v_FfNNzYlFWU(J1|&c_j8ZhHnt4QU@PdI;M67@jAB=soTol@2_%>Y&`ufI_)H)O)Qly
zT>T3D-#1yDG>qsrL7$!_)B9|H!IjXTaXfC!DEVuDtZSq*d~&3Kaa}aL1-kTj{f5W~F-f%m9kLmWbfSh*+ng`BMWL&TWxm96-M3
z1Sz;DcyNhA*}z3qhb#)|)P}61o)lJ*|2&cF7V1LxN!{+FPW=(h!9UP@htNfQ#{H{b
zP!sf?l-nCLN57_HY$4BQ3Z;RwL@JYL4S9nyuN5Ng4I%L&j~P<0Q>3h)A=P0JNw&{$
z&yEzeWhbs$wjtGd5Q(-u^qmGMRG*NW13%xS(E7G@50T_F?QcX5h3NMjheV-EJDJ@O
zV*jN3N}>*9$aEc(Vqd27IO0yWka}JxLVZDD`iP_^QXHNO$uj{nnO-~DPRE^;bV0t$
z0@CPx&bgNQ&7(EqHGQ6euE{D&{7K25e~C8DKHYHMj@l!oZ=}yA
z61}jEn)9UE&(5JNa9R{_)mbL!byBl?s8S!IHS8k{X+IOeenExf5sFV9q1yI)eeNIk
zPALDu3KaZ;QR+P}ty>u`!!or+WQ!`lRU|t+LayrsDoK$gIrJiv-Y@o^qfq`0DaEfT
zf({K4B`L3(&~>z3+(%8wTQr{EqmcM5>I42N>4Ca)2e=>i1@|w1Phsv$v}$%~`)$+(
zzmgm-tGzP6S!AmW^gNGpBI+z6xJ*)@?2V9aKTe;wfa}(zQtf&X`{xD;$&-mFZ=LC(
zM>mSxSBNB^6Nx?{GA6+oVAY2_)jZvVjA)M7L{0b{
zo%13JJ!eoIxQ3eGHRvMW(Yd`LmHG<0n73%YctB)(2z~qq6bCGzJ?bs)+CC+s9ieOb
zO3pjqbDVB2Q>gOi-1Pw|*pKLp{24C_e#AiHk0>~~H(Y6BR`RL}6#SZ?*O*V_IL(+!
z{TD^OwuHQ+aGGiYcx~M}m$G)cLJv2q_pelG1#eqDCutZ92naJfON{F!YJPp#pQ0z4)
z?M*4RBgpX>CuKPyQ)8TSWd)mTI}ELDAGG$pq;l!|l2T2uc}T=MMEeYhZ$b)fljk{2
z1U`p+w|S&GJx8%8h2Zo#1@wEas}XnY`{?&sB-;!jkq9%_;|1=KYUN^8rs@Tev=M3c
zBhcE=b}q|A)MKP(pP|xslL&cC+SeMx*3lTbiX!hBQTMgyRwd-`y0VM5m_2mF(Ye!g
zYKt+GQvHOs*gaCPTj;*Lht}{nbi|eE?=e;U
zlX);v8Cg}J;8%?ln?ZHD-MEQKj#X=!&jPp|sfNh3J^Ced;U-BJ6nYye?B~`hBay=<
z>WCog&%Z-c#1UGekI)%?EWV+gM6#`ndLU0VgA7u!Tv<<7jiSVFiHLAmh_cdeQwm=RXC6t&
zU+lU{g!mX*B0Kh2V8YFJofSgN;DVIhfE3HJRgXXKa#u8YVdm8(7T1lf+$NV0h@
zeXQxK5jw_W$={ZGt;@04lYzG@^fb~aaFqHB|$*U?*@LPfU
z8|@#8{f*iRzZL0w&2$+;ZP2=ezPhLlDZJ<|yp#f0Y2X}Mqu)S(?ErO=Cdnx_h8>|P
zY#;UKj?jDk3z5hNv_%uiM7%_G$R_Q(i@I~KNa1nQ{WIhenPxhTN&zj42#`AllI)+z
z2rv616niXFC{CgIsryK_A0%~aK&s;q%Kg?!Wlqq(FC-^gva|lLEFgnHlX3+tKr&klag0epy0QNmhin3jUnrG
zP2p>#4Es@eb^-Zb6VMS!Hk{i=y?Td8caunS9gnqUw8tFDAVG5kg})b%(G>E%cnx%1
zqR=?{E$Sn`qtJLCO&4BE(|tXW5G%imvok30m?okk0uNZC*Onwtnqc(=_v{T)mFJM0
z+oL#7SsA!NA^JFy9iAb@W=KA}+;dHeX6cS&@}0C+Po>kM
zk*-5a)F#RTh@gFVpn``YUZRA~fzP`&`jBo&`)H4QPsF-UukF!|hR=Tjts(Ew5xs*F
zQvXGs({xVDXb9diHHMg!ys82PzXz218!f5=R!mHUMZS|1)|+tu(k_L;q*|liqMFoJ
z=f%%xzp@K`ycr!ae?dpoPiT!erqK2idT)Fo;yp$cZCB*Ggs#{lv|f0Raw4GKtNWq=
zn}T1VKKMInmn!y{MODB$DNdabCAU{`=*~T^Om3w*>Iqn{1ZOUjBh&%-DroMbbAeAju|Cc|}@2=j?_B&3ll=5#}W+X7NZ
zS*O!}_v}YWl`hJDxsJ1>u(`PP0!`uU6JSJ{zY&cT=9l@-)Ad+GXY9T#u~HZI22B@t
z>3V&U9BSv4w}*dyk?{O*ad_1#?5#qLNotpy2n2T;D-;ZSaz*%zqB$
z>RA-}Orb)(Bn2AIqu#%IB$G&-chz6|5&D?FqAlt(+B9Z#UOPlR&)A3WNP6JG6)y1X
zpf%D&q_jaH{vyhFd^B)@NNrYz9B!O^AYpr!>zJ6zTtBH7<;teuT(rvbn39PoE;ywT
z`Q>{}BhPhCUQaqRK*wB_^}*5{264x>k5np8J{hE^H`{576srLl6z*rL#*ldGvGmMl
z5n&elEQ+^66{%w;b{#3qMC(3DLGVhcm%nY6ylo~OubR%kniPEfxw&YX0t{kH|f?J3_qa~ckG~#bWq=z!4)f%;rhV!qXi++bf3bD&c
zxiy~OAVtd_uOp-|hltRIQRFcvrYLMMQ{*>`yAF?0;l(C41KPi=yQA
zDd|a7&7e@4`{`It&yhl;cuVrIqteQi?au90Q!-l1#jYeLQlkz={K>V3@Aw}*-<$3>H*D0jhjY!V)mQ9z8#&Rlvy9e08tH5=MRPMMGpbAI{
zr`irtm~Rvnnqb?DZ0BiGuk%Q8d4dv8Qj%`-k{;mpDs}@a@S3LI4dB6wo3xMgysD;U
z{Pwnu9?1?*kx0t6A#@#OzD(u=bc_k;FTFwg#T^v-&p>~TZYUSc=#Dp|>+&bGXx@{u
zKQQa#54E)#lac~Zpg_TY50$|inpVv_Q>*3!p4|EweOLd22b!PIL+Y(2=m1R@KBDL9
zPo(bNqATtYr2(r%I`2vKy^*{nw=k7@Eh5u(Sb9qHJV+tBE+9`e2lhZwV$+D2b3G@C
zEC*yHHplfJz63<(N!CQ*J}*$_wSilwdJy~PCZyA6CtCI+mB_V#4Y7%!a~zFC-UgHh
z&Y>Y>19|S_XpZD@;C0lU+d+M}33U-BI@iylTnQY_kX$8qB2)*g(EHz^#*h77
znZzE+iU@2V%>^o672)O?y(~wQ>oO|~D(1N?kcu@Bnev$I91-9!GTcUpC|^hm)s0h~
za;y@M6>+ZO@mMZ~@%U?!^#Bs>dL&)IT?$OX9QxMKq+?7<5lhx0vwbQA&)x!e
zNilP~SatA%OqgZ67*Oav30=e%YJykL5VcL@x`X!Ek7x`(94_@&TB{T&Q1DMcZMgYF
zZP17Ldi4=1{Xd{9>Sxr29H2VHgx1K9XrV`S@GDdWZAoFLI%o+c{?kOp8$wP+9F{v7
zP@tml-gQ!PpX_rQZ>g77D4rf;MVo3jOkw$|7`5=~3d!_4o2+mOAxAYO4*#WIt3;xM
zQUqf+tyqf&$)ED%R+=M|=71EmxW6^UaY*`Ib6t$c^&Lln#~doWwk3Cao3=?OMa_c*
zoNvu>8xz%9;6JovXbovznZ@|&&jYrmd6tjK*4
zU78(Khs~l{y^Fin{kR|ZnjNyt`R<
zdlO_k%%Iqloxq;px>c795^$^6bt}De4ctEU5Y52{NK^HrR=rL)f=Lv5O`-V$6ZNpZ
zRK0#e`HL%1py2-uecGQ-=%Nqm+AhC`F8Tu+LibR4b{n-suEoC7Vh&U7zb-jUcHLs@
zJ~nRQu7C^*w|Taoi%#MZ;QXAz^)1}A?3Hjo{&WZOT;^nufX%eIbD+eVkFzM&g;yOr%5vLPp8FKi>_(Azx=-A;_;ntCWu;plNXpk|O~!8XJ!X-3rk_-;frz5*2iR#sV6pg_Sd6xG4&>h@@piI+S{aeOT4fozW5)2
z#GS%!&lNFUNhT%AD*)uUOd`j5nh3C8icdEzdt@Y)yj>wou+hI)706cPg&9aTuY8Nu>nS5DAFCd;*dG(w#
zr`e5YYgNh+fC2>yekEuOTT`_}Zg%Imj#Ajaj0(SHBF28{HRWOx6WnzQ?^A7grGiBn
zL5=uhIpQt!qFmYBrNDFMt39F0fE4>-Sr(i<2zVHPC%rf=Q0coRBwHS^Ecshb4aiCd
zr+H1Tr*!;bWVso{RqHNo&t~1V>g{2j`cR{>s8vW+fdU1;PSmQ`PxM@QqfU1k94_}>
zm$s+dR=r4fG$74xOnO^W9S3D~fZL}Y%TnLmubSpGfP8OKwXPE~rpjw#C0aj}@SY7<
zcx07Hl}BH%pX?U@ST?@SRvGEI2C*&Fp6)||`+^J{q}V(k&UH6x`v6HY%ga|Zzzs+eRs|9MaKTx`lZlikqEY5R%}gn7?6;ktN*;b3zPA!(+?J|S$5`SJ5H+=g{nY-g5Mn~Jhr|m
z@tjwcc&%s>tRLj%yUz`$+6@igv3<0Y=`dxEx44hEZ(GE$MQh!MT<2L_`nJ)W?rhje
zw0^vkV*ji=%WbqST{WU*)0rz4?cZoE<`ptkpg@5F1qyzP_zyN4`RKUL%sc=9002ov
JPDHLkV1myZcL)Fg
literal 0
HcmV?d00001
diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/client/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/src/assets/vite.svg b/client/src/assets/vite.svg
new file mode 100644
index 000000000..5101b674d
--- /dev/null
+++ b/client/src/assets/vite.svg
@@ -0,0 +1 @@
+Vite
diff --git a/client/src/index.css b/client/src/index.css
new file mode 100644
index 000000000..2c84af069
--- /dev/null
+++ b/client/src/index.css
@@ -0,0 +1,111 @@
+:root {
+ --text: #6b6375;
+ --text-h: #08060d;
+ --bg: #fff;
+ --border: #e5e4e7;
+ --code-bg: #f4f3ec;
+ --accent: #aa3bff;
+ --accent-bg: rgba(170, 59, 255, 0.1);
+ --accent-border: rgba(170, 59, 255, 0.5);
+ --social-bg: rgba(244, 243, 236, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+
+ --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 18px/145% var(--sans);
+ letter-spacing: 0.18px;
+ color-scheme: light dark;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --text: #9ca3af;
+ --text-h: #f3f4f6;
+ --bg: #16171d;
+ --border: #2e303a;
+ --code-bg: #1f2028;
+ --accent: #c084fc;
+ --accent-bg: rgba(192, 132, 252, 0.15);
+ --accent-border: rgba(192, 132, 252, 0.5);
+ --social-bg: rgba(47, 48, 58, 0.5);
+ --shadow:
+ rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
+ }
+
+ #social .button-icon {
+ filter: invert(1) brightness(2);
+ }
+}
+
+body {
+ margin: 0;
+}
+
+#root {
+ width: 1126px;
+ max-width: 100%;
+ margin: 0 auto;
+ text-align: center;
+ border-inline: 1px solid var(--border);
+ min-height: 100svh;
+ display: flex;
+ flex-direction: column;
+ box-sizing: border-box;
+}
+
+h1,
+h2 {
+ font-family: var(--heading);
+ font-weight: 500;
+ color: var(--text-h);
+}
+
+h1 {
+ font-size: 56px;
+ letter-spacing: -1.68px;
+ margin: 32px 0;
+ @media (max-width: 1024px) {
+ font-size: 36px;
+ margin: 20px 0;
+ }
+}
+h2 {
+ font-size: 24px;
+ line-height: 118%;
+ letter-spacing: -0.24px;
+ margin: 0 0 8px;
+ @media (max-width: 1024px) {
+ font-size: 20px;
+ }
+}
+p {
+ margin: 0;
+}
+
+code,
+.counter {
+ font-family: var(--mono);
+ display: inline-flex;
+ border-radius: 4px;
+ color: var(--text-h);
+}
+
+code {
+ font-size: 15px;
+ line-height: 135%;
+ padding: 4px 8px;
+ background: var(--code-bg);
+}
diff --git a/client/src/main.jsx b/client/src/main.jsx
new file mode 100644
index 000000000..b9a1a6dea
--- /dev/null
+++ b/client/src/main.jsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.jsx'
+
+createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/client/vite.config.js b/client/vite.config.js
new file mode 100644
index 000000000..7b7d2adae
--- /dev/null
+++ b/client/vite.config.js
@@ -0,0 +1,14 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3001',
+ changeOrigin: true
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 000000000..46efa0892
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,48 @@
+require('dotenv').config();
+const express = require('express');
+const cors = require('cors');
+
+const app = express();
+app.use(cors());
+
+const YANDEX_API = 'https://api.rasp.yandex-net.ru/v3.0';
+const API_KEY = process.env.YANDEX_RASP_KEY;
+
+app.get('/api/rasp/:endpoint', async (req, res) => {
+ try {
+ let { endpoint } = req.params;
+
+ endpoint = endpoint.replace(/^\/+|\/+$/g, '') + '/';
+
+ const queryParams = new URLSearchParams({
+ apikey: API_KEY,
+ lang: 'ru_RU'
+ });
+
+ Object.keys(req.query).forEach(key => {
+ if (key !== 'apikey' && key !== 'lang') {
+ queryParams.append(key, req.query[key]);
+ }
+ });
+
+ const url = `${YANDEX_API}/${endpoint}?${queryParams.toString()}`;
+ console.log('Запрос к Яндексу:', url);
+
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`Yandex API ${response.status}: ${errorText}`);
+ }
+
+ const data = await response.json();
+ res.json(data);
+
+ } catch (err) {
+ console.error('Proxy error:', err.message);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+const PORT = 3001;
+app.listen(PORT, () => console.log(`Прокси-сервер запущен: http://localhost:${PORT}`));
\ No newline at end of file
diff --git a/server/package-lock.json b/server/package-lock.json
new file mode 100644
index 000000000..9a9d8699c
--- /dev/null
+++ b/server/package-lock.json
@@ -0,0 +1,867 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "server",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.0",
+ "express": "^5.2.1"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz",
+ "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
new file mode 100644
index 000000000..7791455ee
--- /dev/null
+++ b/server/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "commonjs",
+ "dependencies": {
+ "cors": "^2.8.6",
+ "dotenv": "^17.4.0",
+ "express": "^5.2.1"
+ }
+}
From 544089caff096ff3c97dd08bdb8f24308d8e0860 Mon Sep 17 00:00:00 2001
From: mashulik <125238211+FursovaMashaa@users.noreply.github.com>
Date: Sun, 5 Apr 2026 16:23:46 +0400
Subject: [PATCH 2/4] =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20=D0=96?=
=?UTF-8?q?=D0=94=20=D1=81=D1=82=D0=B0=D0=BD=D1=86=D0=B8=D0=B9=20=D1=81=20?=
=?UTF-8?q?=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B5?=
=?UTF-8?q?=D0=B9=20=D0=BF=D0=BE=20=D0=A1=D0=B0=D0=BC=D0=B0=D1=80=D1=81?=
=?UTF-8?q?=D0=BA=D0=BE=D0=B9=20=D0=BE=D0=B1=D0=BB=D0=B0=D1=81=D1=82=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/src/App.css | 226 +++++++++++++-----------------------------
client/src/App.jsx | 173 ++++++++++++--------------------
server/index.js | 239 ++++++++++++++++++++++++++++++++++++++-------
3 files changed, 334 insertions(+), 304 deletions(-)
diff --git a/client/src/App.css b/client/src/App.css
index f90339d8f..71a77ee7b 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -1,184 +1,90 @@
-.counter {
- font-size: 16px;
- padding: 5px 10px;
- border-radius: 5px;
- color: var(--accent);
- background: var(--accent-bg);
- border: 2px solid transparent;
- transition: border-color 0.3s;
- margin-bottom: 24px;
-
- &:hover {
- border-color: var(--accent-border);
- }
- &:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
- }
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
}
-.hero {
- position: relative;
-
- .base,
- .framework,
- .vite {
- inset-inline: 0;
- margin: 0 auto;
- }
-
- .base {
- width: 170px;
- position: relative;
- z-index: 0;
- }
-
- .framework,
- .vite {
- position: absolute;
- }
-
- .framework {
- z-index: 1;
- top: 34px;
- height: 28px;
- transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
- scale(1.4);
- }
-
- .vite {
- z-index: 0;
- top: 107px;
- height: 26px;
- width: auto;
- transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
- scale(0.8);
- }
+.app {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ min-height: 100vh;
+ background: #f5f5f5;
}
-#center {
- display: flex;
- flex-direction: column;
- gap: 25px;
- place-content: center;
- place-items: center;
- flex-grow: 1;
+.header {
+ background: #2196F3;
+ color: white;
+ padding: 1rem;
+ text-align: center;
+}
- @media (max-width: 1024px) {
- padding: 32px 20px 24px;
- gap: 18px;
- }
+.main {
+ padding: 1rem;
+ max-width: 800px;
+ margin: 0 auto;
}
-#next-steps {
+.search-form {
display: flex;
- border-top: 1px solid var(--border);
- text-align: left;
-
- & > div {
- flex: 1 1 0;
- padding: 32px;
- @media (max-width: 1024px) {
- padding: 24px 20px;
- }
- }
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
- .icon {
- margin-bottom: 16px;
- width: 22px;
- height: 22px;
- }
+.search-input {
+ flex: 1;
+ padding: 0.75rem;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ font-size: 1rem;
+}
- @media (max-width: 1024px) {
- flex-direction: column;
- text-align: center;
- }
+.search-btn {
+ padding: 0.75rem 1.5rem;
+ background: #2196F3;
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ cursor: pointer;
}
-#docs {
- border-right: 1px solid var(--border);
+.search-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
- @media (max-width: 1024px) {
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
+.results {
+ background: white;
+ border-radius: 12px;
+ padding: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
-#next-steps ul {
+.stations-list {
list-style: none;
- padding: 0;
- display: flex;
- gap: 8px;
- margin: 32px 0 0;
-
- .logo {
- height: 18px;
- }
-
- a {
- color: var(--text-h);
- font-size: 16px;
- border-radius: 6px;
- background: var(--social-bg);
- display: flex;
- padding: 6px 12px;
- align-items: center;
- gap: 8px;
- text-decoration: none;
- transition: box-shadow 0.3s;
-
- &:hover {
- box-shadow: var(--shadow);
- }
- .button-icon {
- height: 18px;
- width: 18px;
- }
- }
-
- @media (max-width: 1024px) {
- margin-top: 20px;
- flex-wrap: wrap;
- justify-content: center;
-
- li {
- flex: 1 1 calc(50% - 8px);
- }
-
- a {
- width: 100%;
- justify-content: center;
- box-sizing: border-box;
- }
- }
}
-#spacer {
- height: 88px;
- border-top: 1px solid var(--border);
- @media (max-width: 1024px) {
- height: 48px;
- }
+.station-item {
+ padding: 0.75rem 0;
+ border-bottom: 1px solid #eee;
}
-.ticks {
- position: relative;
- width: 100%;
+.station-item:last-child {
+ border-bottom: none;
+}
- &::before,
- &::after {
- content: '';
- position: absolute;
- top: -4.5px;
- border: 5px solid transparent;
- }
+.empty {
+ text-align: center;
+ color: #666;
+ padding: 2rem;
+}
- &::before {
- left: 0;
- border-left-color: var(--border);
+/* Адаптив для мобильных */
+@media (max-width: 600px) {
+ .search-form {
+ flex-direction: column;
}
- &::after {
- right: 0;
- border-right-color: var(--border);
+
+ .search-btn {
+ width: 100%;
}
-}
+}
\ No newline at end of file
diff --git a/client/src/App.jsx b/client/src/App.jsx
index b2bf2e82e..573f827ca 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,121 +1,76 @@
import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from './assets/vite.svg'
-import heroImg from './assets/hero.png'
import './App.css'
function App() {
- const [count, setCount] = useState(0)
+ const [stations, setStations] = useState([])
+ const [searchQuery, setSearchQuery] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ const handleSearch = async (e) => {
+ e.preventDefault()
+ if (!searchQuery.trim()) return
+
+ setLoading(true)
+ try {
+ const response = await fetch(`/api/rasp/find-stations?name=${encodeURIComponent(searchQuery)}®ion=samara`)
+ const data = await response.json()
+ setStations(data.stations || [])
+ } catch (err) {
+ console.error('Ошибка поиска:', err)
+ alert('Не удалось загрузить данные')
+ }
+ setLoading(false)
+ }
return (
- <>
-
-
-
-
Get started
-
- Edit src/App.jsx and save to test HMR
-
-
- setCount((count) => count + 1)}
- >
- Count is {count}
-
-
+
+
+ Расписание электричек
+
-
+
+
-
-
-
-
-
-
Documentation
-
Your questions, answered
-
-
-
-
-
-
-
Connect with us
-
Join the Vite community
-
-
-
+ {stations.length > 0 && (
+
+
Найдено станций: {stations.length}
+
+ {stations.map((station) => {
+ const lat = Number(station.latitude)
+ const lng = Number(station.longitude)
+
+ return (
+
+ {station.title}
+
+
+ Код: {station.code} |
+ Координаты: {isNaN(lat) ? 'N/A' : lat.toFixed(4)}, {isNaN(lng) ? 'N/A' : lng.toFixed(4)}
+
+
+ )
+ })}
+
+
+ )}
-
-
- >
+ {stations.length === 0 && searchQuery && !loading && (
+ Ничего не найдено
+ )}
+
+
)
}
-export default App
+export default App
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index 46efa0892..09c078027 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,48 +1,217 @@
-require('dotenv').config();
-const express = require('express');
-const cors = require('cors');
+require('dotenv').config()
+const express = require('express')
+const cors = require('cors')
-const app = express();
-app.use(cors());
+const app = express()
+app.use(cors())
+app.use(express.json())
-const YANDEX_API = 'https://api.rasp.yandex-net.ru/v3.0';
-const API_KEY = process.env.YANDEX_RASP_KEY;
+const BASE_URL = 'https://api.rasp.yandex-net.ru/v3.0'
+const API_KEY = process.env.YANDEX_RASP_KEY
-app.get('/api/rasp/:endpoint', async (req, res) => {
+let stationsCache = []
+let cacheTimestamp = null
+const CACHE_TTL = 24 * 60 * 60 * 1000
+
+async function yandexRequest(path, params = {}) {
+ const url = new URL(BASE_URL + path)
+ url.searchParams.set('apikey', API_KEY)
+ url.searchParams.set('lang', 'ru_RU')
+
+ Object.entries(params).forEach(([key, value]) => {
+ if (value !== undefined && value !== null && value !== '') {
+ url.searchParams.set(key, String(value))
+ }
+ })
+
+ const res = await fetch(url.href)
+
+ if (!res.ok) {
+ const text = await res.text()
+ throw new Error(`Yandex error ${res.status}: ${text}`)
+ }
+
+ return res.json()
+}
+
+async function loadStations() {
+ const data = await yandexRequest('/stations_list/', {})
+ const results = []
+
+ for (const country of data.countries || []) {
+ for (const region of country.regions || []) {
+ for (const settlement of region.settlements || []) {
+ for (const station of settlement.stations || []) {
+ const code = station.codes?.yandex_code
+ const title = station.title || ''
+ const transportType = String(station.transport_type || '').toLowerCase()
+ const lat = station.latitude
+ const lng = station.longitude
+
+ if (!code || !title || lat == null || lng == null) continue
+ if (transportType !== 'train' && transportType !== 'suburban') continue
+
+ results.push({
+ code,
+ title,
+ region: region.title || '',
+ settlement: settlement.title || '',
+ latitude: lat,
+ longitude: lng,
+ stationType: station.station_type || '',
+ transportType: station.transport_type || ''
+ })
+ }
+ }
+ }
+ }
+
+ stationsCache = results
+ cacheTimestamp = Date.now()
+ console.log(`Загружено ${stationsCache.length} ЖД станций`)
+}
+
+app.get('/api/rasp/find-stations', async (req, res) => {
try {
- let { endpoint } = req.params;
-
- endpoint = endpoint.replace(/^\/+|\/+$/g, '') + '/';
-
- const queryParams = new URLSearchParams({
- apikey: API_KEY,
- lang: 'ru_RU'
- });
-
- Object.keys(req.query).forEach(key => {
- if (key !== 'apikey' && key !== 'lang') {
- queryParams.append(key, req.query[key]);
+ const query = String(req.query.name || '').trim().toLowerCase()
+ const region = req.query.region
+
+ if (query.length < 2) {
+ return res.json({ stations: [] })
+ }
+
+ const now = Date.now()
+ if (!stationsCache.length || !cacheTimestamp || (now - cacheTimestamp) > CACHE_TTL) {
+ console.log('📦 Загрузка списка станций...')
+ await loadStations()
+ }
+
+ let filtered = stationsCache.filter(station => {
+ const searchText = `${station.title} ${station.settlement} ${station.region}`.toLowerCase()
+ return searchText.includes(query)
+ })
+
+ if (region === 'samara') {
+ const SAMARA_BBOX = {
+ minLon: 49.5, maxLon: 51.0,
+ minLat: 52.8, maxLat: 53.8
}
- });
+ filtered = filtered.filter(s =>
+ s.latitude >= SAMARA_BBOX.minLat && s.latitude <= SAMARA_BBOX.maxLat &&
+ s.longitude >= SAMARA_BBOX.minLon && s.longitude <= SAMARA_BBOX.maxLon
+ )
+ }
+
+ filtered.sort((a, b) => {
+ const aTitle = a.title.toLowerCase()
+ const bTitle = b.title.toLowerCase()
+ if (aTitle === 'самара' && bTitle !== 'самара') return -1
+ if (bTitle === 'самара' && aTitle !== 'самара') return 1
+ return aTitle.localeCompare(bTitle)
+ })
+
+ res.json({ stations: filtered.slice(0, 50) })
- const url = `${YANDEX_API}/${endpoint}?${queryParams.toString()}`;
- console.log('Запрос к Яндексу:', url);
+ } catch (error) {
+ console.error('Ошибка поиска:', error.message)
+ res.status(500).json({ message: error.message })
+ }
+})
- const response = await fetch(url);
+app.get('/api/rasp/schedule', async (req, res) => {
+ try {
+ const { station_code, date } = req.query
+ if (!station_code) {
+ return res.status(400).json({ error: 'Требуется station_code' })
+ }
- if (!response.ok) {
- const errorText = await response.text();
- throw new Error(`Yandex API ${response.status}: ${errorText}`);
+ const data = await yandexRequest('/schedule/', {
+ station: station_code,
+ date: date || new Date().toISOString().slice(0, 10),
+ transport_types: 'suburban'
+ })
+
+ res.json(data)
+ } catch (error) {
+ console.error('Ошибка расписания:', error.message)
+ res.status(500).json({ message: error.message })
+ }
+})
+
+app.get('/api/rasp/route', async (req, res) => {
+ try {
+ const { from, to, date } = req.query
+ if (!from || !to) {
+ return res.status(400).json({ error: 'Требуются from и to' })
}
- const data = await response.json();
- res.json(data);
+ const data = await yandexRequest('/schedule/', {
+ from,
+ to,
+ date: date || new Date().toISOString().slice(0, 10),
+ transport_types: 'suburban'
+ })
+
+ res.json(data)
+ } catch (error) {
+ console.error('Ошибка маршрута:', error.message)
+ res.status(500).json({ message: error.message })
+ }
+})
+
+app.get('/api/rasp/nearest', async (req, res) => {
+ const lat = Number(req.query.lat)
+ const lng = Number(req.query.lng)
+
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
+ return res.status(400).json({ message: 'Нужны корректные lat и lng' })
+ }
+
+ try {
+ const data = await yandexRequest('/nearest_stations/', {
+ lat,
+ lng,
+ distance: 25,
+ limit: 10,
+ transport_types: 'train,suburban'
+ })
+
+ const stations = (data.stations || []).map(station => ({
+ code: station.code || station.codes?.yandex_code || '',
+ title: station.title || '',
+ latitude: Number(station.lat || station.latitude || 0),
+ longitude: Number(station.lng || station.longitude || 0),
+ distance: Number(station.distance || 0)
+ }))
+
+ res.json({ stations })
+ } catch (error) {
+ res.status(500).json({ message: error.message })
+ }
+})
+
+app.get('/api/rasp/:endpoint', async (req, res) => {
+ try {
+ let { endpoint } = req.params
+ endpoint = endpoint.replace(/^\/+|\/+$/g, '') + '/'
- } catch (err) {
- console.error('Proxy error:', err.message);
- res.status(500).json({ error: err.message });
+ const data = await yandexRequest(endpoint, req.query)
+ res.json(data)
+ } catch (error) {
+ console.error('Proxy error:', error.message)
+ res.status(500).json({ message: error.message })
}
-});
+})
+
+const PORT = 3001
-const PORT = 3001;
-app.listen(PORT, () => console.log(`Прокси-сервер запущен: http://localhost:${PORT}`));
\ No newline at end of file
+loadStations().then(() => {
+ app.listen(PORT, () => {
+ console.log(`Прокси-сервер запущен: http://localhost:${PORT}`)
+ })
+}).catch(error => {
+ console.error('Не удалось загрузить станции:', error)
+ app.listen(PORT, () => {
+ console.log(`Сервер запущен без кэша: http://localhost:${PORT}`)
+ })
+})
\ No newline at end of file
From 3c27e860da6667bffb08e06cc5c5ede1cf3482d5 Mon Sep 17 00:00:00 2001
From: mashulik <125238211+FursovaMashaa@users.noreply.github.com>
Date: Mon, 6 Apr 2026 00:09:39 +0400
Subject: [PATCH 3/4] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
=?UTF-8?q?=D0=BD=D0=B0=20=D0=BA=D0=B0=D1=80=D1=82=D0=B0=20=D0=B8=20=D0=BE?=
=?UTF-8?q?=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20?=
=?UTF-8?q?=D1=80=D0=B0=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20?=
=?UTF-8?q?=D0=BF=D0=BE=D0=B5=D0=B7=D0=B4=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F?=
=?UTF-8?q?=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE=D0=B9=20?=
=?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D1=86=D0=B8=D0=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/package-lock.json | 205 +++++++++++++++++++++++++++++
client/package.json | 1 +
client/src/App.css | 55 +++++++-
client/src/App.jsx | 61 ++++++---
client/src/components/Map.jsx | 129 ++++++++++++++++++
client/src/components/Schedule.jsx | 80 +++++++++++
server/index.js | 16 ++-
7 files changed, 526 insertions(+), 21 deletions(-)
create mode 100644 client/src/components/Map.jsx
create mode 100644 client/src/components/Schedule.jsx
diff --git a/client/package-lock.json b/client/package-lock.json
index 5deaea429..8a4c5c3ed 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"axios": "^1.14.0",
+ "ol": "^10.8.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0"
@@ -590,6 +591,12 @@
"url": "https://github.com/sponsors/Boshen"
}
},
+ "node_modules/@petamoriken/float16": {
+ "version": "3.9.3",
+ "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz",
+ "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==",
+ "license": "MIT"
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
@@ -895,6 +902,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/rbush": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz",
+ "integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
@@ -941,6 +954,16 @@
}
}
},
+ "node_modules/@zarrita/storage": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.1.4.tgz",
+ "integrity": "sha512-qURfJAQcQGRfDQ4J9HaCjGaj3jlJKc66bnRk6G/IeLUsM7WKyG7Bzsuf1EZurSXyc0I4LVcu6HaeQQ4d3kZ16g==",
+ "license": "MIT",
+ "dependencies": {
+ "reference-spec-reader": "^0.2.0",
+ "unzipit": "1.4.3"
+ }
+ },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -1286,6 +1309,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/earcut": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
+ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
+ "license": "ISC"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.331",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz",
@@ -1584,6 +1613,12 @@
}
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -1705,6 +1740,25 @@
"node": ">=6.9.0"
}
},
+ "node_modules/geotiff": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-3.0.5.tgz",
+ "integrity": "sha512-OWcL9S9+yDZ6iAlXMt32T1iwUApJM8UiD47xbm6ZP1h33d10fqkPs14EG/ttT5EnefpZSx3G15iDFC5FxUNUwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@petamoriken/float16": "^3.9.3",
+ "lerc": "^3.0.0",
+ "pako": "^2.0.4",
+ "parse-headers": "^2.0.2",
+ "quick-lru": "^6.1.1",
+ "web-worker": "^1.5.0",
+ "xml-utils": "^1.10.2",
+ "zstddec": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=10.19"
+ }
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1990,6 +2044,12 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/lerc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
+ "integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww==",
+ "license": "Apache-2.0"
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -2393,6 +2453,33 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/numcodecs": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/numcodecs/-/numcodecs-0.3.2.tgz",
+ "integrity": "sha512-6YSPnmZgg0P87jnNhi3s+FVLOcIn3y+1CTIgUulA3IdASzK9fJM87sUFkpyA+be9GibGRaST2wCgkD+6U+fWKw==",
+ "license": "MIT",
+ "dependencies": {
+ "fflate": "^0.8.0"
+ }
+ },
+ "node_modules/ol": {
+ "version": "10.8.0",
+ "resolved": "https://registry.npmjs.org/ol/-/ol-10.8.0.tgz",
+ "integrity": "sha512-kLk7jIlJvKyhVMAjORTXKjzlM6YIByZ1H/d0DBx3oq8nSPCG6/gbLr5RxukzPgwbhnAqh+xHNCmrvmFKhVMvoQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/rbush": "4.0.0",
+ "earcut": "^3.0.0",
+ "geotiff": "^3.0.2",
+ "pbf": "4.0.1",
+ "rbush": "^4.0.0",
+ "zarrita": "^0.6.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/openlayers"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2443,6 +2530,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pako": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -2456,6 +2549,12 @@
"node": ">=6"
}
},
+ "node_modules/parse-headers": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
+ "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==",
+ "license": "MIT"
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2476,6 +2575,18 @@
"node": ">=8"
}
},
+ "node_modules/pbf": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
+ "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -2535,6 +2646,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
+ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
+ "license": "MIT"
+ },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@@ -2554,6 +2671,33 @@
"node": ">=6"
}
},
+ "node_modules/quick-lru": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
+ "integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
+ "license": "ISC"
+ },
+ "node_modules/rbush": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz",
+ "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "quickselect": "^3.0.0"
+ }
+ },
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
@@ -2613,6 +2757,12 @@
"react-dom": ">=18"
}
},
+ "node_modules/reference-spec-reader": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/reference-spec-reader/-/reference-spec-reader-0.2.0.tgz",
+ "integrity": "sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==",
+ "license": "MIT"
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2623,6 +2773,15 @@
"node": ">=4"
}
},
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
"node_modules/rolldown": {
"version": "1.0.0-rc.12",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
@@ -2783,6 +2942,18 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/unzipit": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/unzipit/-/unzipit-1.4.3.tgz",
+ "integrity": "sha512-gsq2PdJIWWGhx5kcdWStvNWit9FVdTewm4SEG7gFskWs+XCVaULt9+BwuoBtJiRE8eo3L1IPAOrbByNLtLtIlg==",
+ "license": "MIT",
+ "dependencies": {
+ "uzip-module": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -2824,6 +2995,12 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/uzip-module": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/uzip-module/-/uzip-module-1.0.3.tgz",
+ "integrity": "sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==",
+ "license": "MIT"
+ },
"node_modules/vite": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
@@ -2902,6 +3079,12 @@
}
}
},
+ "node_modules/web-worker": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
+ "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
+ "license": "Apache-2.0"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2928,6 +3111,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/xml-utils": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
+ "integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==",
+ "license": "CC0-1.0"
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -2948,6 +3137,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zarrita": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.6.2.tgz",
+ "integrity": "sha512-8IV+2bWt5yiHNVK9GVEVK1tscpqDcJj8iz5cIKFOiWiWYUsK4V5njgMtnpkvKu6L7K+Og6zUShd8f+dwb6LvTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@zarrita/storage": "^0.1.4",
+ "numcodecs": "^0.3.2"
+ }
+ },
"node_modules/zod": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
@@ -2970,6 +3169,12 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
+ },
+ "node_modules/zstddec": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.2.0.tgz",
+ "integrity": "sha512-oyPnDa1X5c13+Y7mA/FDMNJrn4S8UNBe0KCqtDmor40Re7ALrPN6npFwyYVRRh+PqozZQdeg23QtbcamZnG5rA==",
+ "license": "MIT AND BSD-3-Clause"
}
}
}
diff --git a/client/package.json b/client/package.json
index 47757480d..fc34325ba 100644
--- a/client/package.json
+++ b/client/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"axios": "^1.14.0",
+ "ol": "^10.8.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.14.0"
diff --git a/client/src/App.css b/client/src/App.css
index 71a77ee7b..abd277c79 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -78,7 +78,6 @@
padding: 2rem;
}
-/* Адаптив для мобильных */
@media (max-width: 600px) {
.search-form {
flex-direction: column;
@@ -87,4 +86,58 @@
.search-btn {
width: 100%;
}
+}
+
+.station-item.selected {
+ background: #e3f2fd;
+ border-left: 4px solid #2196F3;
+}
+
+.selected-station {
+ animation: fadeIn 0.3s ease-in;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(-10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.schedule-container {
+ background: white;
+ border-radius: 12px;
+ padding: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ margin-top: 1rem;
+}
+
+.schedule-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 0.5rem;
+ font-size: 0.95rem;
+}
+
+.schedule-table th {
+ text-align: left;
+ padding: 0.5rem;
+ border-bottom: 2px solid #eee;
+ color: #555;
+}
+
+.schedule-table td {
+ padding: 0.5rem;
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.schedule-table tr:last-child td {
+ border-bottom: none;
+}
+
+@media (max-width: 600px) {
+ .schedule-table {
+ font-size: 0.85rem;
+ }
+ .schedule-table th, .schedule-table td {
+ padding: 0.4rem;
+ }
}
\ No newline at end of file
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 573f827ca..13dd58665 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,10 +1,13 @@
import { useState } from 'react'
import './App.css'
+import MapComponent from './components/Map'
+import Schedule from './components/Schedule'
function App() {
const [stations, setStations] = useState([])
const [searchQuery, setSearchQuery] = useState('')
const [loading, setLoading] = useState(false)
+ const [selectedStation, setSelectedStation] = useState(null)
const handleSearch = async (e) => {
e.preventDefault()
@@ -15,6 +18,7 @@ function App() {
const response = await fetch(`/api/rasp/find-stations?name=${encodeURIComponent(searchQuery)}®ion=samara`)
const data = await response.json()
setStations(data.stations || [])
+ setSelectedStation(null)
} catch (err) {
console.error('Ошибка поиска:', err)
alert('Не удалось загрузить данные')
@@ -22,6 +26,11 @@ function App() {
setLoading(false)
}
+ const handleStationSelect = (station) => {
+ setSelectedStation(station)
+ setSearchQuery(station.title)
+ }
+
return (
)
diff --git a/client/src/components/Map.jsx b/client/src/components/Map.jsx
new file mode 100644
index 000000000..888d6c321
--- /dev/null
+++ b/client/src/components/Map.jsx
@@ -0,0 +1,129 @@
+import { useEffect, useRef } from 'react'
+import Map from 'ol/Map'
+import View from 'ol/View'
+import TileLayer from 'ol/layer/Tile'
+import OSM from 'ol/source/OSM'
+import VectorLayer from 'ol/layer/Vector'
+import VectorSource from 'ol/source/Vector'
+import Feature from 'ol/Feature'
+import Point from 'ol/geom/Point'
+import { fromLonLat } from 'ol/proj'
+import { Style, Circle as CircleStyle, Fill, Stroke, Text } from 'ol/style'
+
+function MapComponent({ stations, selectedStation, onStationSelect }) {
+ const mapRef = useRef(null)
+ const vectorSourceRef = useRef(null)
+ const mapInstanceRef = useRef(null)
+ const selectedFeatureRef = useRef(null)
+
+ useEffect(() => {
+ if (!mapRef.current) return
+
+ vectorSourceRef.current = new VectorSource()
+
+ const map = new Map({
+ target: mapRef.current,
+ layers: [
+ new TileLayer({
+ source: new OSM()
+ }),
+ new VectorLayer({
+ source: vectorSourceRef.current,
+ style: function(feature) {
+ const station = feature.get('station')
+ const isSelected = selectedStation?.code === station?.code
+
+ return new Style({
+ image: new CircleStyle({
+ radius: isSelected ? 12 : 8,
+ fill: new Fill({
+ color: isSelected ? '#FF5722' : '#2196F3'
+ }),
+ stroke: new Stroke({
+ color: 'white',
+ width: 2
+ })
+ }),
+ text: new Text({
+ text: '🚆',
+ offsetY: isSelected ? -20 : -15,
+ scale: isSelected ? 1.8 : 1.5
+ })
+ })
+ }
+ })
+ ],
+ view: new View({
+ center: fromLonLat([50.12, 53.20]),
+ zoom: 10
+ })
+ })
+
+ mapInstanceRef.current = map
+
+ map.on('click', (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature)
+ if (feature) {
+ const station = feature.get('station')
+ if (station && onStationSelect) {
+ onStationSelect(station)
+ }
+ }
+ })
+
+ map.on('pointermove', (evt) => {
+ const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature)
+ if (feature) {
+ map.getTargetElement().style.cursor = 'pointer'
+ } else {
+ map.getTargetElement().style.cursor = ''
+ }
+ })
+
+ return () => {
+ map.setTarget(null)
+ }
+ }, [onStationSelect])
+
+ useEffect(() => {
+ if (!vectorSourceRef.current) return
+
+ vectorSourceRef.current.clear()
+
+ if (!stations?.length) return
+
+ stations.forEach(station => {
+ if (station.latitude && station.longitude) {
+ const feature = new Feature({
+ geometry: new Point(fromLonLat([station.longitude, station.latitude])),
+ station: station
+ })
+ vectorSourceRef.current.addFeature(feature)
+ }
+ })
+
+ if (selectedStation && mapInstanceRef.current) {
+ const view = mapInstanceRef.current.getView()
+ view.animate({
+ center: fromLonLat([selectedStation.longitude, selectedStation.latitude]),
+ zoom: 13,
+ duration: 500
+ })
+ }
+ }, [stations, selectedStation])
+
+ return (
+
+ )
+}
+
+export default MapComponent
\ No newline at end of file
diff --git a/client/src/components/Schedule.jsx b/client/src/components/Schedule.jsx
new file mode 100644
index 000000000..d676dfb96
--- /dev/null
+++ b/client/src/components/Schedule.jsx
@@ -0,0 +1,80 @@
+import { useState, useEffect } from 'react'
+
+function Schedule({ station }) {
+ const [schedule, setSchedule] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+useEffect(() => {
+ if (!station?.code) return
+
+ const fetchSchedule = async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ console.log('Запрос расписания для:', station.code)
+ const res = await fetch(`/api/rasp/schedule?station_code=${station.code}`)
+ const data = await res.json()
+
+ if (!res.ok) throw new Error(data.error || 'Ошибка сервера')
+ if (data.fallback) throw new Error('API вернул ошибку')
+
+ const items = (data.schedule || []).map(item => ({
+ title: item.thread?.title || item.thread?.short_title || 'Поезд',
+ number: item.thread?.number || '',
+ departure: item.departure ? new Date(item.departure).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '-',
+ arrival: item.arrival ? new Date(item.arrival).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '-',
+ platform: item.platform || item.departure_platform || '',
+ carrier: item.thread?.carrier?.title || ''
+ }))
+
+ setSchedule(items)
+ } catch (err) {
+ console.error('Ошибка загрузки расписания:', err)
+ setError(err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchSchedule()
+}, [station.code])
+
+ if (!station) return null
+ if (loading) return Загрузка расписания...
+ if (error) return Ошибка: {error}
+ if (schedule.length === 0) return Расписание на сегодня пустое
+
+ return (
+
+
📅 Расписание: {station.title}
+
+
+
+
+ Поезд
+ Отправление
+ Прибытие
+ Платформа
+
+
+
+ {schedule.map((train, idx) => (
+
+
+ {train.title} {train.number && `№${train.number}`}
+ {train.carrier && {train.carrier}
}
+
+ {train.departure}
+ {train.arrival}
+ {train.platform || '—'}
+
+ ))}
+
+
+
+
+ )
+}
+
+export default Schedule
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index 09c078027..d55c8cefd 100644
--- a/server/index.js
+++ b/server/index.js
@@ -124,17 +124,23 @@ app.get('/api/rasp/schedule', async (req, res) => {
if (!station_code) {
return res.status(400).json({ error: 'Требуется station_code' })
}
-
+
+ const today = new Date().toISOString().slice(0, 10)
+ const requestDate = date || today
+
+ console.log(`Запрос расписания для станции ${station_code} на ${requestDate}`)
+
const data = await yandexRequest('/schedule/', {
station: station_code,
- date: date || new Date().toISOString().slice(0, 10),
- transport_types: 'suburban'
+ date: requestDate,
+ transport_type: 'suburban'
})
-
+
+ console.log('Расписание получено:', data.schedule?.length || 0, 'поездов')
res.json(data)
} catch (error) {
console.error('Ошибка расписания:', error.message)
- res.status(500).json({ message: error.message })
+ res.status(500).json({ error: error.message, fallback: true })
}
})
From bdeec244794b83f752d755a82f08bc5bc7d0aed4 Mon Sep 17 00:00:00 2001
From: mashulik <125238211+FursovaMashaa@users.noreply.github.com>
Date: Mon, 6 Apr 2026 17:59:43 +0400
Subject: [PATCH 4/4] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?=
=?UTF-8?q?=D0=BD=D0=BE=20=D1=80=D0=B0=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD?=
=?UTF-8?q?=D0=B8=D0=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
client/src/App.css | 208 +++++++++++++------------
client/src/App.jsx | 164 ++++++++++---------
client/src/components/RouteResults.jsx | 68 ++++++++
client/src/components/RouteSearch.jsx | 85 ++++++++++
client/src/components/Schedule.jsx | 2 +-
server/index.js | 29 ++--
6 files changed, 374 insertions(+), 182 deletions(-)
create mode 100644 client/src/components/RouteResults.jsx
create mode 100644 client/src/components/RouteSearch.jsx
diff --git a/client/src/App.css b/client/src/App.css
index abd277c79..cfeb2467f 100644
--- a/client/src/App.css
+++ b/client/src/App.css
@@ -1,143 +1,153 @@
-* {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
+:root {
+ --primary: #2563eb;
+ --primary-hover: #1d4ed8;
+ --bg: #f8fafc;
+ --card-bg: #ffffff;
+ --text: #0f172a;
+ --text-light: #64748b;
+ --border: #e2e8f0;
+ --error: #ef4444;
+ --success: #10b981;
+ --radius: 12px;
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
-.app {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- min-height: 100vh;
- background: #f5f5f5;
+* { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ font-family: system-ui, -apple-system, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ line-height: 1.5;
}
+.app { min-height: 100vh; display: flex; flex-direction: column; }
+
.header {
- background: #2196F3;
+ background: var(--primary);
color: white;
- padding: 1rem;
+ padding: 1.5rem 1rem;
text-align: center;
+ box-shadow: var(--shadow);
}
+.header h1 { font-size: 1.75rem; margin-bottom: 0.5rem; }
.main {
- padding: 1rem;
- max-width: 800px;
+ flex: 1;
+ max-width: 900px;
+ width: 100%;
margin: 0 auto;
+ padding: 2rem 1rem;
}
-.search-form {
+.card {
+ background: var(--card-bg);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ padding: 1.5rem;
+ margin-bottom: 2rem;
+}
+.card-title {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
display: flex;
+ align-items: center;
gap: 0.5rem;
- margin-bottom: 1rem;
}
-.search-input {
+.search-bar {
+ display: flex;
+ gap: 0.75rem;
+}
+.search-bar input {
flex: 1;
- padding: 0.75rem;
- border: 1px solid #ddd;
- border-radius: 8px;
+ padding: 0.8rem 1rem;
+ border: 2px solid var(--border);
+ border-radius: var(--radius);
font-size: 1rem;
+ outline: none;
+ transition: 0.2s;
}
-
-.search-btn {
- padding: 0.75rem 1.5rem;
- background: #2196F3;
+.search-bar input:focus { border-color: var(--primary); }
+.search-bar button {
+ padding: 0 1.5rem;
+ background: var(--primary);
color: white;
border: none;
- border-radius: 8px;
- font-size: 1rem;
+ border-radius: var(--radius);
+ font-weight: 600;
cursor: pointer;
}
+.search-bar button:hover { background: var(--primary-hover); }
-.search-btn:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.results {
- background: white;
- border-radius: 12px;
- padding: 1rem;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+.map-wrapper {
+ border-radius: var(--radius);
+ overflow: hidden;
+ border: 1px solid var(--border);
+ height: 400px;
+ margin-bottom: 1.5rem;
}
-.stations-list {
+.station-list {
list-style: none;
+ max-height: 300px;
+ overflow-y: auto;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
}
-
.station-item {
- padding: 0.75rem 0;
- border-bottom: 1px solid #eee;
-}
-
-.station-item:last-child {
- border-bottom: none;
-}
-
-.empty {
- text-align: center;
- color: #666;
- padding: 2rem;
-}
-
-@media (max-width: 600px) {
- .search-form {
- flex-direction: column;
- }
-
- .search-btn {
- width: 100%;
- }
+ padding: 0.8rem 1rem;
+ border-bottom: 1px solid var(--border);
+ cursor: pointer;
+ transition: 0.2s;
}
-
+.station-item:hover { background: #f1f5f9; }
+.station-item:last-child { border-bottom: none; }
.station-item.selected {
- background: #e3f2fd;
- border-left: 4px solid #2196F3;
-}
-
-.selected-station {
- animation: fadeIn 0.3s ease-in;
+ background: #eff6ff;
+ border-left: 4px solid var(--primary);
}
+.station-item small { color: var(--text-light); }
-@keyframes fadeIn {
- from { opacity: 0; transform: translateY(-10px); }
- to { opacity: 1; transform: translateY(0); }
+.route-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ margin-bottom: 1rem;
}
-
-.schedule-container {
- background: white;
- border-radius: 12px;
- padding: 1rem;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- margin-top: 1rem;
+.route-input {
+ width: 100%;
+ padding: 0.8rem;
+ border: 2px solid var(--border);
+ border-radius: var(--radius);
+ font-size: 1rem;
}
-
-.schedule-table {
+.route-btn {
width: 100%;
- border-collapse: collapse;
- margin-top: 0.5rem;
- font-size: 0.95rem;
+ padding: 0.8rem;
+ background: var(--success);
+ color: white;
+ border: none;
+ border-radius: var(--radius);
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
}
+.route-btn:hover { opacity: 0.9; }
-.schedule-table th {
+.table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
+.table th, .table td {
text-align: left;
- padding: 0.5rem;
- border-bottom: 2px solid #eee;
- color: #555;
-}
-
-.schedule-table td {
- padding: 0.5rem;
- border-bottom: 1px solid #f0f0f0;
+ padding: 0.75rem;
+ border-bottom: 1px solid var(--border);
}
+.table th { background: #f8fafc; color: var(--text-light); font-size: 0.85rem; text-transform: uppercase; }
-.schedule-table tr:last-child td {
- border-bottom: none;
+@media (max-width: 600px) {
+ .route-grid { grid-template-columns: 1fr; }
+ .search-bar { flex-direction: column; }
}
-@media (max-width: 600px) {
- .schedule-table {
- font-size: 0.85rem;
- }
- .schedule-table th, .schedule-table td {
- padding: 0.4rem;
- }
-}
\ No newline at end of file
+.error { color: var(--error); margin-top: 0.5rem; font-size: 0.9rem; }
+.empty { text-align: center; padding: 2rem; color: var(--text-light); }
\ No newline at end of file
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 13dd58665..f0f577ea8 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -2,103 +2,125 @@ import { useState } from 'react'
import './App.css'
import MapComponent from './components/Map'
import Schedule from './components/Schedule'
+import RouteSearch from './components/RouteSearch'
+import RouteResults from './components/RouteResults'
function App() {
- const [stations, setStations] = useState([])
- const [searchQuery, setSearchQuery] = useState('')
+ const [currentStations, setCurrentStations] = useState([])
+ const [knownStations, setKnownStations] = useState([])
+ const [query, setQuery] = useState('')
const [loading, setLoading] = useState(false)
const [selectedStation, setSelectedStation] = useState(null)
+ const [routeSegments, setRouteSegments] = useState(null)
+ const [routeError, setRouteError] = useState(null)
const handleSearch = async (e) => {
e.preventDefault()
- if (!searchQuery.trim()) return
-
+ if (!query.trim()) return
+
setLoading(true)
+ setRouteSegments(null)
+ setRouteError(null)
+ setSelectedStation(null)
+
try {
- const response = await fetch(`/api/rasp/find-stations?name=${encodeURIComponent(searchQuery)}®ion=samara`)
- const data = await response.json()
- setStations(data.stations || [])
- setSelectedStation(null)
- } catch (err) {
- console.error('Ошибка поиска:', err)
- alert('Не удалось загрузить данные')
- }
- setLoading(false)
- }
+ const res = await fetch(`/api/rasp/find-stations?name=${encodeURIComponent(query)}®ion=samara`)
+ const data = await res.json()
+ const found = data.stations || []
+
+ setCurrentStations(found)
- const handleStationSelect = (station) => {
- setSelectedStation(station)
- setSearchQuery(station.title)
+ setKnownStations(prev => {
+ const combined = [...prev, ...found]
+ return [...new Map(combined.map(item => [item.code, item])).values()]
+ })
+ } catch {
+ alert('Ошибка сети. Проверьте подключение.')
+ } finally {
+ setLoading(false)
+ }
}
return (
-
+
- {stations.length > 0 && (
-
- )}
+ {currentStations.length > 0 && (
+ <>
+
+
+
+
+ Станции в регионе ({currentStations.length})
+
+ {currentStations.map(st => (
+ setSelectedStation(st)}
+ >
+ {st.title}
+
+ {st.latitude.toFixed(4)}, {st.longitude.toFixed(4)}
+
+ ))}
+
+
- {selectedStation && (
-
-
Выбрана: {selectedStation.title}
-
Код: {selectedStation.code}
-
Координаты: {Number(selectedStation.latitude).toFixed(4)}, {Number(selectedStation.longitude).toFixed(4)}
-
+ {selectedStation && (
+
+ Расписание: {selectedStation.title}
+
+
+ )}
+ >
)}
- {stations.length > 0 && (
-
-
Найдено станций: {stations.length}
-
- {stations.map((station) => (
- handleStationSelect(station)}
- style={{ cursor: 'pointer' }}
- >
- {station.title}
-
-
- Код: {station.code} |
- Координаты: {Number(station.latitude).toFixed(4)}, {Number(station.longitude).toFixed(4)}
-
-
- ))}
-
-
+ {knownStations.length > 0 && (
+
+ Построение маршрута
+
+ Выберите станции отправления и назначения из списка найденных.
+
+ {
+ setRouteSegments(segments)
+ setRouteError(error)
+ }}
+ />
+
+
)}
- {stations.length === 0 && searchQuery && !loading && (
- Ничего не найдено
+ {currentStations.length === 0 && !loading && (
+
+ Введите название города в поле поиска выше и нажмите «Найти» , чтобы загрузить карту и расписание.
+
)}
- {selectedStation && }
)
diff --git a/client/src/components/RouteResults.jsx b/client/src/components/RouteResults.jsx
new file mode 100644
index 000000000..579e49359
--- /dev/null
+++ b/client/src/components/RouteResults.jsx
@@ -0,0 +1,68 @@
+function RouteResults({ segments, error }) {
+ if (error) {
+ return (
+
+
Не удалось построить маршрут
+
+ {error.includes('Не нашли объект')
+ ? 'Одна из станций не поддерживает поиск маршрутов. Попробуйте выбрать крупную станцию (например, "Самара").'
+ : error}
+
+
+ )
+ }
+
+ if (!segments?.length) return null
+
+ return (
+
+
Найдено рейсов: {segments.length}
+
+
+
+
+ Поезд
+ Время
+ В пути
+
+
+
+ {segments.map((seg, i) => {
+ const departure = seg.departure ? new Date(seg.departure) : null
+ const arrival = seg.arrival ? new Date(seg.arrival) : null
+
+ let durationText = '-'
+ if (departure && arrival) {
+ const diffMins = Math.round((arrival - departure) / 60000)
+ const h = Math.floor(diffMins / 60)
+ const m = diffMins % 60
+ durationText = h > 0 ? `${h}ч ${m}м` : `${m}м`
+ }
+
+ return (
+
+
+ {seg.thread?.title || 'Поезд'} {seg.thread?.number && `№${seg.thread.number}`}
+
+
+ {departure?.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})} —
+ {arrival?.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}
+
+ {durationText}
+
+ )
+ })}
+
+
+
+
+ )
+}
+export default RouteResults
\ No newline at end of file
diff --git a/client/src/components/RouteSearch.jsx b/client/src/components/RouteSearch.jsx
new file mode 100644
index 000000000..9f7a85ba7
--- /dev/null
+++ b/client/src/components/RouteSearch.jsx
@@ -0,0 +1,85 @@
+import { useState } from 'react'
+
+function RouteSearch({ stations, onRouteFound }) {
+ const [from, setFrom] = useState('')
+ const [to, setTo] = useState('')
+ const [date, setDate] = useState(new Date().toISOString().slice(0, 10))
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ const fromStation = stations.find(s => s.title === from)
+ const toStation = stations.find(s => s.title === to)
+
+ if (!fromStation || !toStation) {
+ setError('Пожалуйста, выберите станции из выпадающего списка')
+ onRouteFound(null, 'Станции не выбраны')
+ return
+ }
+
+ setLoading(true)
+ setError(null)
+ try {
+ const res = await fetch(`/api/rasp/route?from=${fromStation.code}&to=${toStation.code}&date=${date}`)
+ const data = await res.json()
+ if (!res.ok) throw new Error(data.error || 'Маршрут не найден')
+ onRouteFound(data.segments || [], null)
+ } catch (err) {
+ setError(err.message)
+ onRouteFound(null, err.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
+export default RouteSearch
\ No newline at end of file
diff --git a/client/src/components/Schedule.jsx b/client/src/components/Schedule.jsx
index d676dfb96..6632faa15 100644
--- a/client/src/components/Schedule.jsx
+++ b/client/src/components/Schedule.jsx
@@ -47,7 +47,7 @@ useEffect(() => {
return (
-
📅 Расписание: {station.title}
+
Расписание: {station.title}
diff --git a/server/index.js b/server/index.js
index d55c8cefd..a7b507523 100644
--- a/server/index.js
+++ b/server/index.js
@@ -82,14 +82,16 @@ app.get('/api/rasp/find-stations', async (req, res) => {
const now = Date.now()
if (!stationsCache.length || !cacheTimestamp || (now - cacheTimestamp) > CACHE_TTL) {
- console.log('📦 Загрузка списка станций...')
+ console.log('Загрузка списка станций...')
await loadStations()
}
- let filtered = stationsCache.filter(station => {
- const searchText = `${station.title} ${station.settlement} ${station.region}`.toLowerCase()
- return searchText.includes(query)
- })
+ let filtered = stationsCache;
+ if (query !== 'самара' && query !== 'самарская область') {
+ filtered = filtered.filter(station =>
+ station.title.toLowerCase().includes(query)
+ );
+ }
if (region === 'samara') {
const SAMARA_BBOX = {
@@ -147,21 +149,26 @@ app.get('/api/rasp/schedule', async (req, res) => {
app.get('/api/rasp/route', async (req, res) => {
try {
const { from, to, date } = req.query
+
if (!from || !to) {
- return res.status(400).json({ error: 'Требуются from и to' })
+ return res.status(400).json({ error: 'Требуются параметры from и to' })
}
-
- const data = await yandexRequest('/schedule/', {
+
+ console.log(`Поиск маршрута: ${from} → ${to} на ${date}`)
+
+ const data = await yandexRequest('/search/', {
from,
to,
date: date || new Date().toISOString().slice(0, 10),
- transport_types: 'suburban'
+ transport_type: 'suburban'
})
-
+
+ console.log('Найдено сегментов:', data.segments?.length || 0)
res.json(data)
+
} catch (error) {
console.error('Ошибка маршрута:', error.message)
- res.status(500).json({ message: error.message })
+ res.status(500).json({ error: error.message })
}
})
Connect with us
+Join the Vite community
++-
+
+
+ GitHub
+
+
+ -
+
+
+ Discord
+
+
+ -
+
+
+ X.com
+
+
+ -
+
+
+ Bluesky
+
+
+
+