From 892d02cf135653523ada96fa0ba37e4880ad0234 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Sat, 24 Jan 2026 21:27:10 +0500 Subject: [PATCH 1/4] added socket notifications --- dev-dist/sw.js | 2 +- public/notification.mp3 | Bin 0 -> 36864 bytes .../common/NotificationDropdown.tsx | 16 ++- src/components/layout/Navbar.tsx | 135 +++++++++++++----- src/context/NotificationContext.tsx | 46 ++++++ src/context/SocketContext.tsx | 27 ++-- src/utils/notificationSound.ts | 44 ++++++ 7 files changed, 216 insertions(+), 54 deletions(-) create mode 100644 public/notification.mp3 create mode 100644 src/utils/notificationSound.ts diff --git a/dev-dist/sw.js b/dev-dist/sw.js index c86c2a032..a4d1b1bda 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.an3a855b9n4" + "revision": "0.n93b853l8n" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/public/notification.mp3 b/public/notification.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..0430b396b34a894384d8ecfc5394159c496ba6c7 GIT binary patch literal 36864 zcmX7uRajKt-^I@q3@~&f!q5#vr#N&g-Qv)#G=epBcO%HqAzgxibax9%Necpkg@E(= z`@j2c@8?|Xi_f$6I_taeR|~2D*+7_oQ8JfaeS{E=7%Aer3Exa7gf=raN6t(&fF}pw zs!NICt}FwB7?F(Lu(*`xj0Jcf-d>6sZCtDoI+k8bA~ubT2? zVCO#m@BZ`WKLsBB{lyn-liXZt*+M>etBy}v+JMy}y-)KK2RX?j^+Kzteb@A1o!Jh> zq1Dz{X@-S}Dt-JVFeHTgsZ{u540+#P!5g9&4S$X&2bGtnantqTKZV968|6KG4_3XN z{7AJ}_anaG>o>=DPA|jiud`V1)xG4so|rgNr>s6KI9i)nKJR}2XQVDshfG}t9zOKY zL&xkXp4|SOW?dMTSuMyD&ICW0;B9cE^A~m0-N>uZgp$ythqCph_8(CXzgbm~7s}&J zIerO(s)P+qP26;2d~c6M<$0Ok<9hu@h5{bS=z&72C_RyXZmq$S6ys8XFZkB;M5zhE zPd@(pYWmsq_q$W%@@!P=-FYmJ9v{Us%YN|U?@WhUf^ zr~B^B2=XUJY3Iv9q?JlJ!|X?ffeEkM>UXH$u1}G*BHe=jTs?p0Yxdroaz~_o`|-wx zt%J^{cWky;JX+Q1g%=JjLA8FwYj9vo^-to z6Jn7HkH-JwUp`67J>V@cG2AnKGIYQw@{_({=^Rx0H}*pZ3s$20!+Q}Tgb5RoT_}49 zqMC(vnSob6=KE%DAu?N6K9uA4z{aCt)~LUUo*TiLoX7dHRHP%1j%ryw;4;OPuR(Hz z?~}HFudO@i`Wp9E6bkKma|n=KeCT_RQ3kLy$}W)>+2O2ZY@U=+Wh+lVQ{Y3PFQwpR zP=o&o}gZ&xH5?Cr;jWCD7*U`i8)YIT!pk!Jd$+$y>*?9-J z6_Q4c0ZYQmz<*2g8xzX<2|^9zGek6>N)*lLYP4wqVe>tX>{ZqW#9#%n87x~PZ#<0I z7gfB8sD0ZEM?ae=x>m+uPWcAAm<1v$-e%cGKbRK%X&(iuy5rjTp_0cXbuU_-=VOin zw6~|OafJ>6gni z=D2g`a3+zdmXIxBh;)H{VLo9eha=T9aFKGHczNLHk) z`RS|WhvGo_pFbcgjHEDk&l*}SpMxLI4wWa_Vq3Mh)$j54hv>(dwKtb6rMow7Fs~bP zXQ!2`k1D-(>?p+57)tf8Fi2_HJ*(p+Y4j?0)3T0#-P0_Vvxs~tLp?!+KB7|cCMm?k zGAGQQgsf2&1zs=d`uhg%^7-;Mgh#&L#?y%H&ZF_#kJHPP5WmY$=$lG{EDOe0#9P;B zoZr^e&o2s2z!^Q&Q_3|qd)bFR&W~w@sw*Wo5KCyh<_KW}J>@a}{meRq0nW$GNDJ?` zYv8I(shKs?P`<3K?=TTzs~YghQOD^!e7I{qJqQuEJ#S~)3(@@G$4}b$`S-P1x?5#R zq(PNy#gw?R?DeCXLZ>j3J`E2jE3IzlcPaM6Qb@h~`b=W@OJdta5(tZ&J)P}hq=vFoS$`=I z9uwZ?Fwrou<2N_ac5u>^-({jJ^)hTOB@0jKG|T1IjbNNHU_ne{T+jePB12I0>14rY z&hp{YgEt?i>;K;3dc{(p=gMtd_xktEM&S4BXOB%N`X9DEZY_sY)+&KTl|7`;}V>^&_5ltClq95s8k*!IZgL-20Mwi@Xx-BO>{)aTq8 zkMF6ZjCLX1q{qjyk34L|Df!iy;t~i%uYWAE-+$6jDcQL7gV+;77P81L=A?MKAkNbvQYVnG z%TSnI-k_PexTW)Je}Vh~KAO~RCoUt8nMQEnxRY2kJAztrUfk*+<8{ifn74m#JUt)Y z+#E^Q;D5h=+%*~P_jbK}$@iVz+xNv^vOaAX=X$87S_lL`68^!>(YffI!_kl3?=V71 zTAL|uh z5_Zc?NR}3&RoM~B)F~^A%4cV~-yWms7^lvL9h{Pn7yxgQKLIez|MAa*CE$xxHFd*) z-|_HHPGSK11P;mePAr5}(B-W(8syT*NVBGsNtzyxQ>RpBw|gaD;A?F9wW+Gq(AnYj z@%tJmMdMQH6nje(v!!2tvj0Xl?l}#GVsk~&SjC`@Na{;S%66-)>~wH@-d!Pice#Z?t2_=% zp(7GD59&l^daP0704L_rm7pg!SX(Q{h}y*V}6Xp}6lD96U(rEsxmSHN%!fT(iji#>zPv6~miI6Th*{u%2b#!0XB<~D>LV)-YHUwNX zT$zK4_j6TKF#)$9lym&SXE96|FT)+TvBrT)<%lK>F(1y6nA~ip_!a9m(zg&*b3`wW49!%H0FuYit_C64d-PDh zwYLa$o^LQ%vN3fbF&z!Z%z=vW5BKmWF00s z0{{L(<1%M8@IcpZ^TAYYPE!=VrwlkK<64Bm+yTu%49rNE9>OS|pbW z$jwW2;aw;^(IRu)eaop@Tuc6D`R^{BbXA+-m}A?;>HhDf3|saL-JfX)>42|^pZdRj zeY$jZ)fO;(`qsPaQ)&Crm(%vsw!aEjZMJLf+4}`v8+S(|Fgh~VpW=#3}Lbj7xjGLT`B^@I5%yN>bOkWv# zS@puQsmo+2bIS6K^X*e+@8Jj6jb&vuSwZ;Am0{Ll=AiBtQ~cWSvxban$)unNy$d}4 zN9{^~=Slakq+i+F4ikwzf}4QnC3h@54*~%ou$~6EId4qv$}s~mdp4LTrvyqw#0|ra z?TxcME*MGkPMS36HIlZiG!@kv)3jvlz7YESU3pHj3$UALk8GZ=tZ9De6bZ|QYQj*ZQoHkAvUQLpmASJC+GA^ zwEyFu5eutU*ECwS_bFB%Wpckk((&nV-N#J#ePY>8xNdl|wX8gj$wJelCDm|bpK5z0 z50!mJjLK^lQQhICcufO`-C>1`Wjkxjw4OiaM#!MihMaMkLanCbNVl}SWOCUZ^iI^i zfX9<*{Td?|`WqW}myLF&-)15mh`yBKMfbY)@xP11l8^e$YA0X6$ZGi|JY;o_VmQPy zfB2Tk7ihDQbp!{XSeQE#KrL1XQqt3G(gl93qoe^li2-oG92_Q!DHEqv5p9ztfsS(; z!~N*lv17&!<~w9YLw~i_mRcx+-)!@ocv<_rNRR-O;-p=DT2H$2f7M9Osy&sKs^Kb) zSUlD?G5u;$JEK%>YBeEb8zsEmA#rEy5hyRl(K*9?qio$gkXdZ_yuDhizA+QpQ|C2v z$>(caTe2R?X564Nq$vHa2Z8~lR&D&DC`zCZ#{dE4gkV@0%(0iFng$G!VOpUwlG&6H zz*hE7l?gAv5~oD*#N?(v8j=0w%L9b^c#>1VwjluGp6>))_8joT~6 z+wJL=C6Y`v+?AS|Jq}9!3DmstQMUTwT?bPu#Va`o;EeCm9(I=CW$ z?tXrnwavj~8z21-aR9Qhrw#0ZF>)9dzzK}@!k81PBSN5wmU^h=HIO?er_Z?B+_{) zzJG2A)z^jRv_>5*p~;#USAMF=?o3e^J1975M(UGV* zAOz(A46#6Hdbk1Ro?Cz_4Je2Lt>({{Lczn%t(IYbpMYG3v{EfJO)QR%lf1YSx3t+23^TzHvF#Lw9~dIW3O{lk=8g!L^Lv|4;LG*h|L`8SapWW0RDwc%ViWVkkSX5U~VqnoP3_#-nzg#;m4Gv97rXIMDqLjUwH9)DHv&$*qilAnxSy+3)| zfSdMWYNj>;x_0-wUCUVFJ>FgSE-c8j*^XO{6s}yz z*zf<*p&uQrYf%zCSn%P9TCLcNbwyiNSKFv$%$sSLMgcnUQO7=2A>Yz;6YtgdxWseS zVGLc6b++%g=n z0CWbpkJq=WK-4<{Kxv_fcetHslIBW;Qce`xR-#~U-%b#AQ=b#K?=bx;(AY`G&|fKt z!KlQAZeqIT_63#fLyA4}ofUyc(KUDVW9f4S+D9Vb=lSM?aT*g`rsl>PhE@N*`s(Y} z;47Z+xhpPrb+zE7_};?f_~mEHA(eiUbg+xg!J+3Ir?h9@y7!-Ub$xq!=t{|xd(-1i$=MgnZ0?w=H8{sdH5TG5bA@oa6FYyk!o1-ORF zC^H1t&z4i#Wxs<4h2mslz+1+!xDVMLhhxcadAm4Tn6e(c@E>Wek`*qFw)?TJC+{)T zxEF>CG?WW9Qni?OvockqdX-!KZG2la$JbAI^7h@0F$>|W`*$VFoy#Rx#{CQ)uB{SP z-j=NV`>>(Fd-}Y=K4;#%^7r|kLirz^?z*oV%i`05z8xFWL4UjNSoA;dZ$BSQDqj|D zj|i{&4p3krnWInjjKnBFLbKRpfPplWF~wz&ff}n7qEEn}gl(@fC(izle;NShYcOdv zAq_MV9cl-0oSXID;S;eH8vBHEkh~Uc)4($D(rKX$7&bC+%UrX6QhqzVWb98g%0Eu;<~Kz1Rj>R2!@FR)bWj@RyHhTrc$i-JoGBiW z$6jg7^?**8Oeh7tUo<`{ZS!Ppkc8#T^&1M7N=2<6k zv{~f_Ku%$SH!NmDLv$|ylo$!N5c>g^%@!I`?D8r578WbPuFJlP`Q$OIE7_~rV-C^< zDZ#-IA<(;)J@cdHij9uNBkjB(wZOkVYHo~?c7_>Y%s^C;ir8^4sba>P@?>x_6e(df-B~akT0|4L*~>(Y{->)7v{)yr#nCkaM$nQS;kCX^Lnr5dS2)G9M38(7s^JWop^{eiAC@){;4PZIavxY`vQuMHzz{C(Z+qe)OtP;iC#ES zJX0uqsPyAKX|3qKYGPelY)=m-SqdAY9sz|mgBw68(3wHWDp%z`j;3|#dg+mQS z-+s%9KgUsKvS!y=x==y1`b>MA$w^o&NjwaoLU4FxCQzYj5V!w1 zC}Iq;E{uXY{v7hmidaAT%zD0o=T-8+!+^Z`!8r`Fp)05V(G~$l`TlEeAbdzs=d@>P&hKl{W@unOc2m{VY zC?N*%ZFbrG*ck~j1S>`Qn0Y`v5$Heu=>b@k;l$Cj_*u2K1yfMi0$r(=NS$X~Um1G| z=`mNEZjT@c0mh;_DOZ#%amtu@!qMf*&(B6adg^WkePqp_Xiy=u$3*qvrX+AnXU~2mnBU58<ZTBQmUIK3FiYLt0IVJN?XSFVk-}$L$Hd+PosWvP|U2`?Mw^I4Bw=zg6UT`_VE=1?s7n|Nya8w-G= z0X|P}5CtGj0U+R51V1zkmufpA7KY=*R8d5VL#n(`jNpKf#GQ)1fx0TQ`T&p?Z^m%$ z^Gg`l@8IsK`#Nm3+PM<|J^8b(?0};yWLUP5xd-B%6no(<2C*l)wh>1}tXpt# z@Ju~5d&$<{vXFbMCnK9)e%}QL+_I;|MGN{tLI7fnI*b4oGG@Q(txk=5jj3fAx;q9A zxOP_O$BwDe^&_1yXsG*5s=gS#&7g!hW8`M;H_yqABYrTK22{7V4E2*!)%J#xn);L}^7{Mw&wC+euOgz@c1 ztN-!Oi6wY!viQ{8{!2mS<&8JT8BL+Mmar&usCUKwkKrQOV|P;9S`}(Hrw12qQ?Vd6 zom+5DCxgrcxt&MZ5G(@-%RopueIpY|+S`67=<(hZTk7a%i=3N7{2kYi^nAd~(nj~(Hc?c)ZPJtjjW6forY0t)H-_j$Y@A*=bzju^ zA1&9}+*~Ut{Edo=dTRv$w}CsYeVij)f<#f3()ANCM(`A>A^E2F+*Z>Ijs~GdJf6@a z8i>TPmO~b@0>ulhBBc9OsnG+C>o3PMMPATf*+}8Le-^<$!9eq*F|Ep*bh9X?ogF(y zXYYVaocryKsNGCEPtU-SL1+pM0V4-(?C8gjA4GG$7qPR`Dn|p+5k+k5?1Yd&2!yT& zz+fJIp8gdWc=HK?uqet$0|5EoAz!ljNuHPSR4mxIF=Zc#-`0Idv?kC?-zTumLlBZ8 zDQc$WW1wbhF;1C(Qu-j{?$mb)yiH{OK3|HY1 zcLYLr!O8{~CymX!#|!15$#g8yT=wqI>so56FHEi5eZHLDvs`&@eb2gmKO&Y(%Ou0n ziLb?=k@PCf#h6E8FR;{5LlsxSPt|>IWd=zF0CxgtH-5H$8C!-MQ`pU_)q6qh5RLr(>iGD7^ge-)Ahb%m!)|6O0FDJsZ2nvyave} zu}iU4P8M>_q*Jrl{?5IM;?9S1thY;>^K!K-Bx~Hw8|O})>~b>OoF3;{)P3Y~G^KSN zVvf~|lOZgwW=iCAZ4s;Fj!mxesC~nuom-?2z+5e)1wM#1geqlN%OWrjQW-X%pou8z ztr@m7Gvc!;hVbM)$aasnCT0V{BJ_F2sNOE~+jdUyX^hCCzE_p*Hg4T1V7!cRC--jM zbN2v8kB20ztiiwYRbhoxiVfx#EiJJpKc-sDGKx>B*EJK;<9q(&Umk#t>CRm}R``=o zOjP4utzq;{OTM z*${IXWVj4n^YJ5pvMYh{3g>%mk$S>aZF^CZ2Ck`nEB?U2BQ3eRej~y1q!dOR-Fo>o z^zCuZM5wg5%<+m^aWSHxdeZ%A zr1tDRQK>>K;1YU=UTp#b3;<}ETPOrTN0=EBzS(z9LqMRZ53kn+F>sRRjB&GE11TVn zUL1FfK9g6;pPZCGd`cEMxL#XlZ6&*?Z!D4LkuOObTEECkcE$h6T%1|?QP*`nFS{^3 zHupK-+w!Gm3&t@blOf6DYP8dIwUtjHUR)s2#eb+9|5sTvsDNdtP)<_9aMp7ErC&nO zrM0!f>u_Voe?|ig2pop7yiCr!tW0&zrhsLK%Yj2sNMnbmd(U&*Fm39ELrMYr$}wf+ zAa!uao7-++B?(V=*qDgf>hpK8SCl)cdw&>q?bqwrZJ?TnxUM~d3!NwQH7&P}judB+ z$tl%OY3c=%Zd{Iy=vv65~|RLmD2I6=}QCX*D#Y&^~6UB2>r)8)xa z8t0T?0EqkT7t?z)5s&i>!@!xYP6N0KI*!6`#Ru^KibRz8*#Ss$ktoB=oUJ$AgD#PA zX`!&O*DrNkXCN&+qG42SXCa6($J0k?n^p0tq#Ko5%JSDVsqm8OYl6fj3sak6BHZSA%KU6f%WmFJv=ru0tQ|=GH&-sPvsnQG7zY9^#9quclV32F)?VfKpGY?F4;s4;@B|M4dpr$mv*gh!P!%X(f#04SVnU zg_y1P2?h)l>caql4bGTlMjzPZUilyYzQ7>p-s}~#og6Oa&@Jd7(a2RxvN|XV?8Dw`Ju6MFQIXvaSV=x-xw%BSmcy#{1rEMp)f{*AQb)nq1YK1D`Yx*UMDQ)NICjl zz}(w#VHk^+v&oUAc_S$I%tTVVW$<3F5kXST!p3xd%SQsaC*S;i1`m=LA}vh4e9N^S zj=ndnpT86CE!gELwU&2zJbLo_X=$sO+_THU&+`%sj_(u#dgo{?%W6eYf2p}*tfKn!y2dk8cYNLFCGbBnGv`H#_D=c~zPMn2n2ar-ICaAx( z{W+1r7zfp05*;orwMat1d?X&{BR~=VT5M6EfOZEJa=Tcp*yGQvs2t0|!2ac6W#sgjX5KdKHCzbBsO3HNCqjH{G z|GIhY(Djq5;;Sr)Gpi)wr&Slh`N`_0G}WKUIq@f%ubQwBsS0Gk-fm(D41k7f5tE(h zMu+2)dyPFv^^`{XjY)Kgfcie++woGWJ^X~+5VS5a1cYNkKxiM_XN#Z*CsVrl^3xjn z#O@8+mW-$pI;4Dnf6uv44pzIs!F8G0M{wJAB=u$dmAU3)i(Kl2&R0^xd4rm+I3(UVor*43?SG2ELI? zS^T>JzRvvpJW@aF1YQ^GKjdvODMY)zDY4c0-R|k`#D_pg2fDo*C09uVz?#x;Ld{r& z>(4O~%!qq%V%axKg0|e;M9SLE)y(Qd*f}Vn3dN$6ggWUqAxadb3AP8YfZ!z-xFzT& z``f|7?vI!cqsOI1-&9)I0xr~ls*uYZGfw1Yqc@^vEX`0H@S@;{`?m`1sH7rt-LUhH z{rmZnKMW5CSgGsBj~+Z9=Al!*Xiaz}vNqA}f7se7)#BbTe!co+Mfu(aDwpKwmd>*%zz)8VYpZD5z3`}GV}so(GW{pOFbtG)Po>aTI1(U&!3 z%kLy72dQ~91RETqHBg*Y`{^dn^COscYj^3y{eSq|!U2F?`K~q9z8X9p1g6OjXF?E6 z)X-sjF0p-#@%KaUu*0|z1FSMzZOKY~4-X+U*Cw+NHVAC8D+b{*5Lnz~1-nDIkGI1Y zrI4eOLLLhN(^@G@zZT4a5pSkN!qSJn7G>Bzs|C?D$-c0_0>p`D0+q&i znv@U$)Imz^o`qvzx=U=HFh98?`whovWXEsP>^L*TG=Wq(*PhahNvF!b`=_t?u6Rij z1)n7LS5CJIcP>Q+$T>%r9J7g{qADl7Ji)`3Z<_+ocDiB$)FlZ35R$m1pGU`t9D%~& zwnZsWXDQpMj4r&mcB}{svpHHQB8*~?;4Z0e*bEH`2F*(aL2nA6h#{))3=R|0tOUc@ z3-Yad8$K zr~ht1pqr=QNd9YUrx3eAlB~Iqs8>~FyPboRJBcP_*fa2W@o_;*mvd$F+c}?K``j7j zvX8sybNN5^Haoxn>BQ)?@sWf;;ogy3u9n$C`I@-1wGWNAJ35>KfFbu|i3Fh!ARr0? z0T36T6#)}~w4TAyGf;%P#+cJH&|@z{kdzFIm7op`h=w2~B$)_;r9gnTvDKUt2(usy zChPe8{G=6QIv+EEsYs0z_lwVFX|wGKISli#PYf#% zNMidR|33h=G=q_&7k2fXS|e+|6Zhzav^eS%X8W#qKdfog3XgjLpxM8$NU^MA_EYky|k&mjm`Mu;oD! z0e1E52vFfBk$VEy4_4J2KH4~~j~9f-4=gDOx3Cg^hmjLeVOO<+(x&u3*^F%R4r(WF z-$NFgkL$5W)qDNC&ry2(?oGwPa+1Y#ds>#Rq4V=!_er0|eEO81HGlLzeP@x8+klT~8 z)?35sM0QEdD#*2U(?b!I(1HKii&P+l8-b8DIamh}%hQAd4d8bjc8t@0fy|?tG z)~0IZv5V)s7{+)0{9mXeD1D?0;&l`5pLA2+SV_0IKe1dpp>Z-bSyeJFvU%CQ)q2SC zBUqe9mSx@C@Z|jWWm51^!C-`h2&<8LDf{XjJF)iz2)8roIGg6&R94~~n9>%Kg5`7b z-SaMDwKq-Hg&08ZjerMwegX$2+ydmV6^H=|x=;|JM}`zZGeI+=a1TM-6W&WhtZSH~ zFzO*R5c18QoyiS^rnPHtme|&sd-EP^plJ0obZ1$$r=o3(tT2A8hY5Qgq zLSZ}25dCqI(QeX3m252}8B6bTOtBi88<(oYwLwiWNyQ(A^-=|pI93{9o(M3e0X1OA zQF1U}TAUgU7y?Gep(wq@JunR@2)GABlmsyP>q3;+a4``}*$m&*1CCqTG=jIu0_k{@ zKKg6cOe6`8MNYzUu7z_uyGh)~M2SD8me{(@ z59kA+@yv%1;EcwuZs+?>t^jLx168{QP5j*~k9DktFK2fOSfzCVI$71033rN4*Is=q zk-V2-1A!KKEzo%GPBJ-?Q&E;FM})cw$X9!cER4r9y_l@Px#_mFt$bEBNYksS)2sqJ z-d4z3(GE_s%NXY2y)NzBjKqJB1)wPjsgOtS*?KSlfYyBi8UlU~dI(K`dr?e)Ixp1C z2ZeBA0-P|P|4DFIh4U1FIt9S~FA-725!JPLk($((0h?o_9_k<&&If ziItV1IOD}(9%?2>zvoM9<$FHPpHq5aH+HrnKJGC7vuRokA2F9Bf99sc48O3)0=*0U zcZ{h1_VpmyUvq;t?J!Gp1V9fzJERFv%0*ctn!r716d#=W_Sm$-Un3HIvHD~~?@#~~ zH9q(EZW>`pn6wPl?cr|=@Bd1}hG|Yj~8NQR=(T6 zE_8gf-*~-z)#WfZA8g@>3=ll>Sbk?|^T|K{+cSg3FISW;^g<;(-Wqfj5~p@{r0<{aX}H?M%={nrX}Mtvvg2YP5ZAz{RjGpPx#3j%B`>NXIDkh~`z_s_~a z*g*@D0Z1fbR^s=zO=qngrCB*nz5@d~5Ae~#yksKl#X-zJkXe|bV zioiSBtBhDn%sphJ5H3<1=s^yP>Ka~1>Wjmmh1d$>Sgbh`2*D}Vcf9<;`zo%7teRV& zpl6UluYcG;bBp#3J*TQreRj2r07$^l_L-{>tuwrsS}dk{DxFao-m0q`XY4=$& zicvgd<#lzObaaBtWJ_uEk{-vqzSOb58Wlr_o0gF?YoUgS!mZ0*dC`npD!oo%kj{$y4(_hvtwqj3dU6fP=HoQR-d?a+8t+#H%baE#dZArX8=@}K^n z1F%uUp(7h@{SM(LQ9)serY0n`dO=Ps;o(8+d8PcPUL9%iSVXS#Udmvn%-9Y-#Bu*6* z58j$~B#kg6&I5?g%bs&u}!2rInY{ya&^VO{FU;z?!t zjeY+Xrgi09a_9~5&`KX0)G=nK>jx|>4xX-UKfA7?!s;( zeE5VHUoSi@ZI%@4QW9t{k9-9W{gdOnZ*%BjYWKoAs-S%87COA7B#~oAn+PB<(n2q^ zT?uYH-hO}L@cyLpefP6p8OC~HD%nZ=Y64g}E$m1r3cL3%mPleu^VO7$c#Ssl`I z5936pP7()q{K7&2MpGFBF!qcg0L_1&UT&t1i-KGquAx53m@A0Z_K`52wI*j_r7o5c zZQC>#;*}FKFSJbEj?*)gs(d? znGxwQ>RrZ$F25_{+eys%+|Q$!@U;2;5%Uf2{vDS)Uo^T-^>WIW-pxNbqR#x%()4II zv-|o;s(bhA>D-&&;>cdekgWaf=dEpN)M!!z?>`^*|MeS5Td0r(<%Cj`jvipN3>?_< z7DKUtw!vQ7C`CSB#t;^i7|0jY!-i5-n%BWtz@Xkv8T~K#2}t@gISgB8Yjw)Cd!D4+ zn#kvpElG-X94OaKEH;r|A(C^r$Dq2fSEN@!Mu8kq7ZmnTCDqRezaV%XYH5NJR0#Et z{-HT-kIsCTS@C6WM;)i8AJ14@MU)-Xn`M^F}0TPm5>EZHc^Nm=&`eO(KA zTx`=(H5X+Oz(Mb7Qm1C!alJqLqk8{Brt#KpSMj;Z%18u%phqstPZf>X-uyKq#d~$i zs;`TP-K`(Eh;%^W5wc~2(gnKtzvYVid~x7^t&r7(e^#hKw=|dsj8?9UI19-JVf(0b znZqY$;M5340M%nn(s#J(m`uJ0Z2mVn;@2~Uqs4ZJyjQ(H>qqyusji&!lsx%nDCV_2 zsKGU)#utxr#p>T7RAs9TnNN--RTt$2vjDU2VN3_AE`RfG@8K?r8tF;Fw!)3+pwIc6bhtA%9ZxJ{AMPlD#`L;Y)2YX_Q zp#qq@lbxrxy)0es{hLl2pxKGT#w#$)Agn$HbstFXVMPH19Jfp{;>5%%ka|iK7^Hj# zK{Er89xc~z9^btrEQ1&YtLrt9Ht+4oCaL+QdC7;L@3&%pzsw{V!;N}=A2&5|AC7WS zd$!{_)XpPh+Pq4Ka}{&=kN4E_yqHL8<|T&5O0Z@>nybh%6n`}rtok-k#Dyb zD!N1_KQ@HqQ!|WUjK&fmCKt<8I-n*VGl>E!V*bsJLo?|w5nzl%p$e&XB3 zhs(djvo7$xRHC^8#=PZ@_0aieNN743@M6o3i$vE`XOo6ph9-A2HWTH9_E0d;g6gm`AeZ{O`Iv zYvbg#oL;zQ{fr$+iysaXo~|TVWczk}$WS&fDPNou)_`h8%_J&>?j@ak@9iXyvvj1W zr44BFA|wJ;Q1y#DcPuGbIu$)Zna*tI=<_lWcv!Qko%y;1@zEmr7eX|IHSPXpZ4UJI zg3Fo&XO%bs6>(b@6e*}5f)N!J5mhn9@}jH&eSKy)R*9#t$1iv8i*`|p5}OaT{fVQY zSEodK+D-{pFg9i-NsorO9zCF67SztFy59L(n9G2Q&)qi`_`P%pyc>xwrt{JKKa$SE zp~?2|;-kApca70CLQ=`W=olR`*yy38g2+R|fFX&i87-HyJ&&@)1H)lMMXbv`PDA9ylL0{8?199%u0ewqJiCyqKYh!;0^EH?kCTk~qwJ>UmR_ZPrziN&-WKu1lM#1q3v z8{_7tG3Q=sXehUikcXxyEnmnL-K8Wlsa>DVq%;8mQvXdko#cMMA<|7Z`DlA1D|x*M`V>qxKnDKOGI@%6gWaTRG+HbHPnQf=_?p1T#3RFM&6vdWX7 zw1rnlP910`8%Bcl{^S2kG#hQmNL!Q$eEuSzt&steD43j z85SyHYBU5O{`*LA%fQ0OZICbS@M)&5040aE`~fX{(u@co4M?08#!eSX%hU}7s2)`5>DUh;VR9jXMkjTJOEXJ^pH%sdqVt!Zn=QpeU`gD!O&3WL-k2>u`KC=oH5v=)wt-793sXxzqSaYdag0vp8Z0lu zlEnRLC8Cfd#t|n^69Er~OlGD&e-NSx1$Zw4j$Td$;Tfmi%E>LED>+FJd%48vq+EKi9FoTGC?!|;D(&W@dCC)m{0cu7_4}7{J{|Du>dz)!;FTw) z27N=IV9{Wc%ISb3c9bhH1Hjl3<7}9oMSq+ACClFj7*3GKL`RNjw9R6N&fXvuBpK3m zB$Os7)g1RYdm1Sz!z#y!0L6?~y z-NL~po?CO=r_`f<$pR$WkakM@4W%G??~Txi!f!-J17Cp9pB;2mZ^9w)!{OyeD+47| zq|YwAL-Qyx0V9ZA>hR?Yiq0UZLFxfMPW@|^LFGI>!%n1edx94bW+d8(J7SFms6+!z zNSCrly_HM=0BzZ=%N-ygWfYi4t~z?2Zxa`AC`@iutFq49gwrNZFM2EBhGos7ZOf+Dd{ zF%#CM$`!#DWeUT^*X=XMg%z2e+0)azYR`Ss{6-FD=bPl7xCEsyriKdFsm0b8Cy{56 zZ%;g-HCuaVt|CD8bs__x-?pOQ`*PYFxj-4uK}AD*J$te*mrOett;;M0i%EM)n$o3g zNZoCqfxa-fZ8}GF`;G3sgY5%ks_I-+MD;%Q1^H}ilNFlraBH$AUKZ!LGh^rC<9)N4XU89D7^_K#T1C1Rd2yjCHF4t7%$( zzhv@|fEcj^4d*@+mLgFon*6SYS6^H-A^|wh)y$iDoVdT=%z zBGkIyH~^Emhmtf4>HE4#QEor`W)1R(OR`9PtS1*Xv;H6d-=h<*LPpf1AYaARUCCHS5XrGjooJ7D3A#o2(Q<{P|!6tV%^eY2lo)3DyJG;2e zR^!-g`!~Nqo)lm{ir9oSNJ*gr`x**iz=LP)Bimck2pG*py`ta3zOf+R*U0T}Ni)vs z=17f~N)Wr0dWo{<=9#s~5HCAQMEM0JL5-d>tRPrU z8KA4vkGZA0e`1(v%VYic>`SKr(VCM$043EDwoCPL2|=!!I0Wd425iK-6Xgrt$kB47 zHQT3urHLiW7SdB92a#z?rWs0%I_PpxE0h^V zk-uxAXvfy(s*9@Q1P}-)vD|@;_-h}$Ld^$m&M;Z$44pV>VlwxUSS|g+w{skn$Bf+6 z`ga4FSb&2Bs_rQ*p>hSO#Dl;gg5YEQjW_%mslL`_YR_Js zV~w)#4rw2a`t7hPHS4wuIBU@78zhqRfo__6K0 z%(u}uQKe5hI)J*Wn|Hdvfl0I7Q?_444S1~p05Ur<66TZ_Hs6k`_AEGbh`7RX^9S|# zOpLYX2jPGj;#=E9l_ITJD<4U`gz1*qDJ9J_X$g@hio>AV4!qZUzn!eDserqv-*a)_ za|}m|fv_symb6sz>zBa0|KtAzz*-+Vved}qHYn2#^Q(ihyR6X2$}JE#sO{E`)5{gJ zBF{>UzfI-+0yqX}tSAb4zfY4qM2URYKOTGZ%XG!t+`C+5DJj^)aRvK}W%|m9kn=+A z#ZmGjI8ev^5$vNfStf}lwd|7HGnOVO-^}qJ1Au+}*MrXKi-!D|YCCIfy^Z37RkRHE7mj zoWS=cYmE5|EhcilPSiBq(;mlTK(x}Y$&_OZsLDoY2hIG$nR~FLNp9;_Ixt#Bya=X< zPZ0p%Z`R`Vu`z-@5dr~%+&WFg5z%fc`M_w)fAqB}Wm3n9OS|1X0ViL5#g&H5#PtlB zNOzEYz*(50o|VJZ;|a%RO6p9O{cq=kWxjj_WMb5%DX&M(mkzJo&Jz!#s7KNjXPpB& zjg#q(YJcu~I6U~+-*M{L62n;{=U4sdc^>|~>X@Ua(`Jpv9JtFT(g|4iI?hF?zeC{JMB<*D#*+XmLrYGa7q)6~O&Ri}zVsrERjfACA43_u}PC2)=P z8kR#%3;=VHWUg4U(hgGqO1a$JC_QA_{Ri!(_(7lh_SvcW z+(Xs>=Kl#sza@CYct+8^i9X*U5S+}$G)rgJvXC;}TShe^8aOSEe|g`pFt2+1pF_9P zwe1NPwoGArND+bGv(OwP5F0!n`}cLXMkf^9xc#c|>B?teLCv?Du~(4oJcI7p?5@ZK`*OtiJ>K zJSm1Y{`~r`h`g~Yg@J#%N6fJ9$5Alh>sRKC&0o9@|HAi}zMsw1cz&79ALH>Y_7o@k zOP2r+F8=*X{;1Q2iCBrfsyg@0SXMNQRn$*@A7Whu>*%AeUv7WoBDMIZ_sy@bQuM@Us9;^H0_sv=w zhOc~7-_?EiLKauBrT#$SRO0!_sk+YearR=Qj*R>1f;ZoI^^X?UJdnUXa$P9$r8}d* z=6=6-&&kcvUzVcF=VIXiEpx_#NdafE7sYdq%L=dM>uX&&Aj6%H2dgMQ zRAc>q?`WQLROOXh@!B42ZCu;B+E&QtN{dwFJe>MS^HVC!@pst)?SzM zHkN~vgNYL3Nmxx6g1#i{+LEWeGzJ8HppsZ_P|Q~REdK;7*>p!0TH4Jcq;0x#*FBX# z;PS1pQ~cg2{Hs>^^uSTy{g&9@!WIJ#I;j07wQRJ{_azq`nZhs^0p<2T{X@A@9I7*o zQ5+nTnGkw#JO6i=$jw^ish8ABC&xLz;zj;+TpK%beX(_sKpnA(n;`KlL^UUU1=E!N7GXBrqr{QhBsq%b0 z75yF+Vf(#~_<5&dyZs}}8_qm6e@OQ4J|(^!hG`p%2&YAh z;U;^C^(_N}!lWQ4_M3_B$jcpZF82Tcp;o zu4f%Bzg#}3mLLAqzAcSLYj%M0j2ACKzo1-?RX>(wuC zcJ(?@FMtvrq4#&hsPR)=l|P0ojnPHLg6EaUth}+-2HE*-dU5B{wZv%q?AvH>abk=^ zJ(mmBu;Mn%-InQf8Jaqifg4#F^q$a|`~D5j8*%6NDs$f;o0nC#?>3!q#pm}N5>-=b z;Y)>iuT77k>}hb&GZE73F0$QZ#y(_2*zm%OU<9Wko#F46GGZYI!ao^jPLrTb>J;(Q zN)HZnmW9o^889eWL~+!oMpo!CtS7Uww({JU+jQlpLv>i7JAGy#E#bpgXE>J}t)zV1 zd&LO^Cn+1=sUv<>t5+eVTWmv0$zV3K8hD$N-FdQ%K;w;6GZqrOxKqNFa?<$b6v$iN zMpXEQKe&HZS=2dZrVFfLNvlk#*$cPC3tzaO{>T3r=5`Y57&n^e5a?^6!9H-iKP#lt zp%N)eJv)<~U``f%n-A>!BsJ*Y_XX-`pduCoJ|cjQ-ozGOE=ipbC%oaa3Cu01wo{I> z-H^7{13lB~)c06e&{5h=vOF1E*{7&GO__)_Vo@+eDFJrZaL|w9kM~|jbIY>$1`{)Ek`(4 z?+^Z}o4hsC(+bBK{THlMd;@ZoZ$&8V72}xQni>P=vfou2mFhzDMFbyBZxTbS>`p8K zWDkL&i1>B1e)g(sOyJJSt*t=wON)#5$*I1dM6Xdk%dKv&i$++8n~RYw7L804(dy<| z2BKKpkT&wybmeNAq74M0KCdR_$VdaXiC8IhkS?&{g+8~b&|mZ>gjklB+==CP+mvDd zQdai-V%mPg(o003F`D=RO+x9n8tn@YX}ld66oANK{OPLuEyYh>3}UQCN61hB@pdgZ zx4)IbF?L_Bjej1_J}5m&ZBy<=!c*&OJ^8fgb3Yz%llf91?1^4v5c3U4KD!qnVO($| zx%Wm(G!YL3ktYa%gH*?>q@%>(;X1r3pSxt#hSX7u67YC_UI#aUOr5cbT<5FI)%Jl6 zfRBuGkTQ)(Zm+wKpZG6_{4M!TbEBT;vWC~o#yaY1DD#;xG+5Le$J0u!qN;$Ze%d_3IYm65YVSVH z67l6|P_Rf|5WfJV&Ni}>TO06;j)+Gcz*h7|-8LKOD2g^Iv90}Ze64Mu=ckPg&6>Os zJB>f8WK(8CJaGm8@qY}+c@{cs+^DG4Vpa=9gHx=<-!L_Uf{kX>b|%}vjB5o3N@?#) zZkVTBCPrl%Jt5nFq2P=g)Wp@DmME?>q|_Tba>Ju(7Cv;tn|PPE^@Hx7m9%9|NT?&Q zni1qQ|3HlX`}HHqw*eDS$7Td{(?XbEz$Go^XjD(JrF0z}$%tr5W6mb{mT3HXDYfTU z-)}nx`PXsllh0Fy6L!gXfkK6z7pxzb9tB9rDMJ0Do$ z`!U7`o#mo9?!`NDSen|YJD-(Z@Opu^z{tH3W8f0RQvr3p`wSI zzxaN*xbZEh>rf%O3SZb|AoH?fzV}M1KiJ0emBmI*TW-)6b5*Lz1Vyj(1Kwx%P*T3_ zx|?jFW`)^CABOcwE)T-?O>|+EUB?cp%(Ol+l2$B!jH`H?Q)+ySFeKdV0yC4ICDuOS zTo;6dT3LB5hbv%W3YuxDXdFq7?1YhlayqbOWC2Nw@#oI+wwNIYg022#W*w0z3)z|| zHFds@TsD9;9lUk-%gIRY&Z$3=gcd-|GD|+%~9Z*y|+5zG0V1=g!Jr!FR?ekZ{mbuDj zdht72tt!=yIot(tl7k9iYD}`JO8SOC7iJ?+2rnXkcp~rFKQ++ogeM*F!v|f zSd-0h;JcyPY6eu-WE=z9vDptTpT?2yE|tf8tZ<9xXw`nrSRTE9&Y5c=%rPRxIKW@C ziUUp~mk#-(Qz!uRapN+?sD49;hoXrYn8;SM)53^&X1!+lUsb08RnJ_dF#LlDq3hcc ziI&18u}!eDRI5oC_e=Lqt49NGr^72UrgbN&iqZ=W`fohvGE5IPT+PA!Q0+S=)pF=ET%cXnG9cK;^s7wf*u@&xG&J2x# zX*SL0G@RTwSXyG|?8edk?$fMq-!yW(a8E5XIpndU#+y5(WIFO^H|p3ZNVto^f*x*e z)d`05yy=P?gCv0H89I3iHp`)pi3hodfv=|RQMsCV9_$^(%*5ak z(7KBKsFc_?K^z}6_h{d!-v9IFMivBO`RyuPR&oEcmEwf^e8ccugKVp>)Sr0R`RjWk z%kD_cY1K7z(e;2*_xHxC!&$;b&&5%=l>!k=c7(+=)s|o7F?ZECYVj30A!DJ`g7yzt9&ztwaAhMF=Ek(pX5_H3+d@Y z6J>%?4sF{vw1Dccd|~zQcGJR*uyB`JC$lRViZ|J|8}I2_lX76p>60`KlEc1v3Fd)^ zMRSKO9Xw-#nJD_Virbo9RELcpEAV;B=gq(FybM-pMm0)A_!-r)fQAiMv^Mo8M<xYQ<*cK-I*Wg*ztxwzzw^p+vcum$C&6jY z@`rR)-F@G#Ek`=nljoHfa6<3(Se5d68kdU2<}5gg)s-A)_CPea>P|kUB=6L1u+?vIsUl-IYd&eQKX!P4|e3jt>mcDS($yXxEoS*{abhHc5gR zByo*><1!Vu7AESuLr~hRTDok?s|Zu$_kC6tX#rH?|MC9`lk+uXi1}9kbj(dC`}tV0 zF)fkLe3wX>yDdbvi7ZI_IqA3hEHt=?2xu>5Z>8~zDZ|t{c8#jK1JlAOid$F5njaT; z=88|B^{P}Kb2!JSwnWg(ycI#exZ~%;FJ;(BArW_o-kB9@?R#*pbPLcY^z!n)K=PrRP4bL_5699 zXB_$HH6__%tHqnq#6$t@(L|s3$2Pis zzRS5?6-Y6~$81i-WSg)k>TS<{{d8o1vL=7phzi>6U7=$rGLVrL{Nmb=uWj)Jr4F{6 zfZZKf*zaZZ&zyC!QmrHiiQ}rH4J2x+n42WHKLr*4P|G-x9YGb zu5Cs?M*BZdEV4+GFsL2b?|$+5q{B;3u%6Zyu6?3TAtSTE_7G22o#qgz{$aXtK<;ti z7$tH2@appnjmPOOhFx{|u}*!urg;`2#2`(^WXsX21rg2tIHi{5@5fI@H%_cv@RWI- z`jn$UPnfVyvKpIwY>)=^GiFwa50|q*NpiSmOKc}$W+74G$NkiM?u}lY_xO1V&EwVH zGqn*CDuP*xm@m`SCw3Q=r%$pr?b0kkeURF6CbDxVA>Sq zXf21kbHctW<-)>_&3>uAN~rTGcOg}iQX58I4_~-EYT$Bx9Dxfz%2gBu%Lxs$^{=%_ zwK!(;`57N0AIL!dF)k>W=2b}Ae_X_WM>zu!UVUHGl`he7HzifO+06m8XGyB{fGJLi z7+2^+dhTF$FHvp!>3E*>di7bcu$6;llPs}5JbWU1I7EPkSM-M#a&_Wiiw^d;qGs4UC0GP~K?-2K;n;rw2P_wdB zDQz!VSQ*R1m;Y%$8M_JnEP4?(o!^(s|5`L{-QvAW-wEz-0pRP7REI%aZNf7a@eP0UAv7Kp!CTY^Cl?E1Ia!?s2~ zN=n0T;PzhZ`ZK5D@vmJu&kh4;=T~d`AKnLMmSd#w9Qv^>KVcr0Qg(&~y4=E$MGuP+ zmro^}$vtS*MD84~2D?DBtIw%bo>#Nr<g0898DgKU5Y1)8K%KKxhc`p8(I@?C2Jc4cCI-%7$f<#d~B(fxfzI$ ztTm!c1_;iGO3@6$yV6Wxby0FVt3-5fLz7}&;u;ROZB_Aj)XCr#vlDZ+c<-OO2z_Je z!2j+4Jf+K-57TB&^xPrHbc^^;zGN3!A~o}oNO_~SgQ*Z%UsK<6VUb*HxWga5=GB!| z1p}FyPMlC2QinOGK-s|x^?ZhnEp<72YvUZkoA359;zX7HSDZG)l#`s_seuEgVm6(x zGiitHt`EK-8T8nyT${)?2Q%Md|MG%?-JCNn5BUL;RFm*j5d~J-Sfx%z_GBU9?;n@y4#Ni zwU2XQ+6M>M7Wz?Vs})caDGNo6BE9dM$`~$h@z$7Dub}}HT%fa&uDxS8_1$61Goqb8 z))GXCT7pjbZ8#gfd7$E{sIlmUeDNt77@rV3e8L-z78E<#IVxDPILY{W{p!8-FEOaX ztV@YiCwn|!hn)=xcp@+|^y;86RI&14W1@q7n}KiIVIc)OZeAmpm3lXv*XMS3gsCc% zaGH~j`PmIMQwFXvQE?TStNgsoC1hT4)DRx5)9uJoj%Km!0aPI>v%sQIhmJhG1!@Hs zC_t@CB43&k;s~I16=YgbJIDj=PifRETo8+y=EpUWbs2jx(st(PzNF2`%d&Vp6l+t0 zC3w3Ta%<5}WREqbjY9k!YaweB@ZY7|70*onY(1+TPOG&l$Gw3zx0=*hh_lEghDeUl z^#&GtUck{_(ngu%TOd4VYrqRt^V*Ky$;e!IlR!Z^DhFg4iuJ9LPvlz99-gd)#??<5 zM%2vmXbX$3vg-)L3BA}!NC6MpDu`E@5$kTGS_!XUI!%xe%%m69B6`k7aAOgWI20xp z?TIIpLsuSO(Cw~>#P8wdqDY4aOJP?Ey-|GRe8#%*KK}x>V)G19G(Bye?P;nyuRh%*(jrZu2$%Q zxRo928nNhC@bFS^DW+QFI6D=l(nJNNRji=Vg!tUiv+#c!Bqm}%OsAbX#h6l16j>T& z3%_ASm*g@krV$hamiN2GDbJh1I+gK$a_M?{#FqAr>lsj=N}vq^>;%21WY-$O)>|)i z?3Bu3wrS9m#3n>j@=Bd^+0mH4Pu%F_E>n^mI-Fziw=gp#)F#x2YXK%KigKOIcG^^hizNWwW%Ozd90@}^V zR#CA8;$5o~6eUP0zs5~g4v*8r4=uCLXBv9n_UB&*0we_h11EY7@1H3wPU)0;QWq@J zRyvj1X5<|WRI*dJeD2r6v9Tel+3cU@6se((K&HL~PyiQB+-bo{u6DX1U^FWMelbfM z)+Ybf*Yh+IxGMi^Ki8nj#;A=#l>7W{kOPY_RWqSp0;XbC|43!5>R-mr)tr)4{ry-Eyil8U#Y7b>3eJLeOQWmvzfzE;M%B*hDh%gfl!+i6kQA5s{j5eZN zpWTnvi8|)I_kCX$%fug0`_QqE+R2HAfauHv>xhH%W?=qVat_F>G}Hd+_)`HMWb~JC z1*Eu?t~~XqwH{VnAl7QwhPqm-eXYOUNktG^EPv%+xqK%I5#gR+O?7*YuD@HR+&_nF zOIx&QQTtktW+G|H!4Y##;6-=-(?4g}9Pcpg%tDw}yV(TThboA@XM@<_T!i>sT|nd#++0%4 zP_nSAEMXvCP6Ai2msFOLoSm`ov|GR%I^x6~;m=}MDx!sF-HZ9dGxS^uYrq9f6h;?H zEF&@A^@b1E*O`ERrV7a!SzHq3Oy^n?*&}qI===oK3Q>?GKUl**A(!3}WMBuj^W_AP zof8khSC@g#v0qj7?^G*>eC?C*n*77^6ub=)vbs(9UZ|kNx)1O4y+G%N+Mmv8!>?*fWphD58 zPC|SvjOt7AW5w%)T{vibR%S9MFd*7aalSvn8$k-*z5*{z@=n}&_Bv@FW1Sh7s#h^N zQ%7nMm}x}kc2zzzD&~M}Vigr9w$V2XX$Np9R^6nGi6IRSxctQgX zWRyyCt4`Vw8+Hdn#3@AWP@U~VrAG3VS2TqH;}UN**4UN%m9Qw~x}Ge+?~Y6+%ghz$ z?E5Pe=j=LHMv`E-y#!=%C$ju6z-_g3d~?Jcpl;^9kRiu`AqHgOu|ec z%Sy+E?g&*%|Ahpf@u!&#d_EibzlVK2eW4i1Frq4j&X6ycG#d3G!u5c`pc?M1I$-4FP{u zVtQxA)jof>%ho~Ndr8(UbAjD+QR{0rGx!P}W-nq2vLR=+pD1k&@vmB1NiFbSSUK!f z6~iwQ(*X0?+}c+ROjoL(>X9MaZ;>aL`u6G}$%@g9sI%PdpL6@m#gW!UA$1YPw()J? z({)gVs@RMDX;rY=F(H|bSI7($;EOlrYBwr!XUvjoKc-Gz10jlFVq#hHt{gqTHDV+x zXb`GQChaPxI_cs)wVj5Q84`6}3Y_)mN44pCKHk={bwZL{-6SkaBEe}Id>Ix$G<1Kg#uROg(m7q)vy_ck2 zXOlRS-(=O;LLg0R;x+Ob^VpXlcujVCLxtCMr1Ht}yBs7hOEg0mDUxh;=7m4=E8gS-> zbQ4t7{!1I@z(iDoKhr*QN#XB|vq~4uF_90-%n6m3Vi^Yi^M5X|t2DR#a~m^`HRlui zNIhnIxE5mTNA3AOI8>z26nTiVZ0O^z&JFaIfdma$a&dz#*^FcUU+|UwQ}z_qvRbz zY}qPZeI4d^qX`Dnnr$;SK|dDH=wMB;VLx7pV8XMICL1iVRxD%y>>i%xP`sJ;Z=d`X zW3g{id?g`Jtw(-HtnU9!`Ir^_ChhU78R*@oEg@#}rMCQ#Z}9DMm~B?Za&DJ6k2dpt zmg+Uk5lkesM$9qlfxgO=l}F@xL|94q4eLonvr^y})JG>TVWIHu5y+@`^`fT$?qRm0 zgIYLLA10J)=@47BVWk{;R$u6t0LbB7CF9U4J%d@d#_13C5E}WGYJqORlf7Al0-MCjN3o6IE&F%2?A3tN1Yi) z$BPZ`$2UhD73%Q~>;0L^h?MjJ}C!`QJ22J`oM4HMgNS(s8dARnTy`H3wD>cYhSc#8C^Ra24K&tJfusVS6S-LGp zfeEFhhO!Bz3YrW3%umc%OhVMBNy|2!Gd@4|NAeGrp1XcM-dmJU^3lBps*|b}`a=3I z=8`S-FROGm@4hV#2)5Eqt>0X{`Zdynyhl!^ydlN;Skk}RG;M@7RSMYs%mrk=D$Ss-zXivAdLP;5Ci-3LH? z4n1IAy~F6-)}GvbHk>=;q2*8#z{y~lJc)FVwa0`&3`|)-Ze{A2>CQIMvrPNf9B#!< z&AG46s^`Y~e1ARfc{(xvwGwi}+OuS=Ci#-1G54#Dv#W8h415F9ylm6k2}vkY3c-J@ z)ubpxh%9W<1_yg@n|~5^q8<(ik+43uqQ^grU-S<)&TfYy;aF9*1p$mqc?nZl%9rcM zTSu5#V0TebQ7h?vbSa>BwaF;!bCrng#9E&C49m$Bl-S~z5^d1#{3o3ZHkUpYv6z;G zL(3a5lrCixDPf|6G6_9Y>Xq~~fv>zZjbDvdQi`k|Q^wC7xlq30Hnxjh2=Si*d!VHB z(m9HUcvWrQ{StYc>txhC|4)`-ee#N1&IH8JeqAwKN3q^UAk0Y7fJ)3dmw_2kD}g0J zvBD8kvMf+2Xgy&z}hTEt|<5kAk?}=YKaM z74DYaC+#HtIZ%9=gD*C?3?G~Cidz3swpE_K=l{5(Fnp`*TA>OYLmh7E2u097pMPG< zC{?s>&^sbVU%OOdK!4ZE*4-p!R}!UCs-FCmK|n8`Kq^rt{Ar^b%T(qlcw|PqAhw_j z+V@`gkN;0}D;}Xk%wXe|&gKarKPoE@!4-hCysXhmZz-%oWMR!rDobC)Aa=MQsVLft zPe)(nwmt<;tLEHdK4%OFPGr0PvZmD)&*A&v;3qr zEy^luAa%17^Z7Qk4yH&W~ zJQ}rqqH5i<7U<_4nz^r95CL<3H`xuEIYGIMw56`KeKHO}&IroA!1IW!AlPr?EnVo% zJK}8)*>W*Fp8owTuz3%-JldGVyJoaW znp*+m%xe|_ZPhY(FDTe;&O_oYm_9w+cey|#^BEUo{sF6=*2s^MY&}5?@OEURs4Lb+ zPf$Z~?#D;15B6!L&o$g#reolT+CN;2xXJ@3sXUp@ zrPMw>iE^E1(xNO;;INeT>Lu6K@6#DKQ&-l8-kt64=Nk!AE-}_ka+rvJLw^i=@gTD! zqMj$ecYRoDq&jlZ(X1OP=^ZuPMyf_)SB@AvGGn9&x&{tUwqic8aZ+`0u6VIaEQNV6 zFe&aW#0<08y0oDBjDOli`#_89zr+S%5)}b;KrWdr&63y_JF3(!vNSJQGXw7}CB z)T|#`!_f+wZix-65c|y98y+Th?P`!=yfLZN%B%qEm1s}^QC0lM|7W_Dm0SHIYdqf{ zFabbGMX~9x5SwtyQZM%YH~)`yUZqOU@`;U<4if~7X3GysTdVWcq7y{1k_T)HA`ASu$27;1gDpb#qL^}cMqR-7K%>$VEx#Q2nir+-3;-p()gZ4o}0o=AK^Jkoj^Nh zY2T3`(%%Iz@{8ObU5Fnttkf3V6}`_DJtMO;atHF2p8#h3&hYa`mSAqAZB8;lDrPF^ zS$x^C3yroAQ>w{&j`SPJFOx!*l#V}mBHfJ|97twM5o)7)J~K<}DklUeE%V3=Aha=K znr)&g3=?mVffG!@@u~^anIPpxvt1uVaJL%PO2Nf!d-D+XSB>Tt(UYD_I%3vI(H8mh zwB>iWs-jOee1s-)rj|{C0#E#3Z21`Ab+-{hu4w_k4JNkS~W7N*S%4VXWfU; zP1e%;*t_rnN&fA__5QOI1Ry@-|L1>>P3T}Mx$&3Q^n5ntWV3z8`oFXy&xVhBcgWH} za^JcQDfEUzx&5m*M&#rI4->d36VuC)-a5SZO-!Qo?^)G_m`Wml7TxUY)M09iqFlgI zZrw{C=R^bx8tTUqQ2Q4LiGDZak25*XD`QV>t2-zN?2F*P{RaYlOtdQdY~{-<l}hDQ!&+>aj?) z;vsBbK{3bLi$hLVuRX9*yz?s5@osoQS;};meLW52Vb8ce(a$_Y5Px*I+%UjvLuLN) zvJkV6eKUe{T&R0Wr_C_=J6Fs_kGpd1vCwHd?cm)sVCirb_9S^3G!zspARG_QaHD!N zr!a1*pVJTkisrniClX>(YcrWcqg!u1;B)iR2e?^)u8i_1ru_qaW`2N2_NTEuid*z# zG5Z&!$@}qAKmt&fOqdD&{E=hYOs=q2_6r=rY>~au6qG5cY*8y!p31LHHk!l$5h$;) zK$fXZ$(P5gE9=b00e3&79t~w`2e@TrO_7<*Dw3_^8}u(Nakl{}7%`JE3F8HENQ0N4 zFoMaVERl&eyTA-A#KXZwM4{`pGK+#r0f?L(S&I4kq7qjcTA`9&K&@AE#l-+AnLkxP6~0E6guS`V4kvyQd9&YOx@D-{>D*GZww+14mR5b}yxZrQ4) zxI^=)?e1;_-b8ev6Z#=iW%+g1%|eorAqEUaS>C}erhd+lpk3uDk>(D z(e79-cjb!-9jnobQ9a*eE2-y$(_)nVmcs6j^2F|E!-!AS3?CMmBsp>)lzvrNR7z{U z-EknzMTWC4URvP<5WmO39`t_We*EbG3aOEO3(rBe^_tWP_;JL&Ep0)3(n~Y+OQ{jd zD`@hd+25Uss9*c1KBTp6{9S1Mb&-R=rhZHNKgVptFWGm(=@i~aId__&kIZ6{u%MEWtn+8%yLI)9iImuDY*?JSqpaV)&w;5cHE=jUkUtcLMm37pcL z&Ux|qym{cC;BfW(A1~m^&vo-3kg=UPE&N8ew`w@oUAzBExwp1D>qFwttM~B4zNB?n zZ7V$^Bb`37cxv7Dc&Sxz;B{jo0razdHg-6{(goMkPepBnzmrUtV=QUTwQAk-bm}|2 zSx&8fDWbo`I$ugwTurvg@mr&&u&lDW82_y=p!E~&FR*WhUsYfU7xlAGyRWMPleb!&=2)&stYG~@C}Uxaz=~zSnMS-y}AoJRscYXU5gR>1y{h{y+YI(yh{lYB7_0 z5PIe&h)`$L?BW~5rkq&f!#)96O{Lo>--9b#cWgz%?|@^EKZum>nPMl8#8%1!9_7BB z?nqpE#Q(K-o!@LYTsTyf)DN+$W{ucVYLvFrh!G7nlG317tOTv7*8B-ot7goQS{19P z8nw5gYD*A=*RE1+jV{%^-uIl}|L}fz@5g(;-TR#9+;g6L?$Zahl+OFK+9HrlDsVHA zJwIiD(J~ylY&>Oewvi|Z(Y@(pZFy3{&kO`zNsX1P80}6>&>TPjp2jDPio@c~&wK{4}&amsLL5z7_1FVBeDwtQN`*~GBh7Axv%M3B6j7eGzqo}&PsW} znec8E5I=HKYOLX8;gjg-vP&Fkoig~KtV>(II$yI2*BMa_en`6O9lSGWce`li_n8UG z@Yf!kZqTu9%7>|Fs*ut`OIY9VLbpBOJ9a$k3#aa&4gKv zLLL=din1Tyl76IYHHJHB`)TbG8tk;ATxdsheNP-Xmg+5GZjyP=k%g^HDmV9C<`@FA zkYU}Cvo$5`PlZ01&-X$ks2q@=Xe)&;&9bUt`Z8@_4|%HP>eDYp36nNlrRi}z3>!rzRYae z$p!0hA@E`d+VV|_k0=lKnT0H@dUpX!3ms0B?^{FR?a={JX`mmz>stn)Uy{eHpKhEy zcS5cIdcIq24Ei*BdEXd@elD}gtuwZ?;BRAVt`a2sO-dWg-_R&M;ilaCAkpHIcfbba zojpfOF`3{i;#H}71YiLG&K%w5a8}iNIx@q2sTm{}9k(gU6mxmxZ#bd`sG!*=#mmil zX^SRCwVQ(vT7z{{64FjYt2uh+mog}i=F|$E@n^ME!cVd$f83vWk_y)Ys7^eK$(_(|UmFCub-nECVvzfT z#T$F@T7G$7kqu3s%DMfq<>3yCzN6L#$PK@PhCesp-^6TW_dcGW-;H$TGQaZ?G%nlt zM&?BXEhMO%1%@o-wXfIlyG1tlp%wzn6~Bb+Lf!XCwY@Lv-6K7$S~jV3ep8FY=**tH zV2Xc>Iu@>Q(bbG~@m}DOJlcY{Zt@@hLB=xJnC=kQ7e+#)?$-`DmQ{lg*Z(R0krj-x z+LI=YG``kVa#XxfbHCmT4>)BQK`4K&LqE~sBC}p`EeoGwRJ4kJmaAz)YE1$staqg) z8ov%)kqVi*3R0WLR~HuN>Y!C#1VJ0!u3>%t7F2~Y7jlZ(jwwmAQSzR^1x{_3+yU(1 zkGb!0d61W$r~^c*1lm;ZR9Lbge4N{z#oT%F*|ca5x9$z+b(AD%7>XJ}#yl7^6^fA; z4c#Lbx78-)jZcT2q{ESWt>62NTIrzhgzt6LLri3vKfwKBF2;M17--7WwC zx?dO1j;aMfensi4bG37PGz<;0xk!FbQ~XUf5AQIUaWTZiP-(1I@#PrJIqrm{zQpOa zByC1k4+oinve*rd)c!o|+?lY{kxnJt^0^V=;Ut@C=nJVgeVkvFw`uo!nN6p}KkshSmh-psbN+aiZTV`aFFTSMoo)1)-`mMa z`oZ&XjXfvD+?pd|Ta9CRuK%nP>Oehf9D!133@pRULZ;i1U&Jfpjn@1T*=`gbz7 z`wIcF>V=@_>hrX>O?$524;re(0&B$N@>gbK`=zAS${B}njiL1K~GqT(DR_XKar^IA(yaz zzKbwW-0ZyJRA-c~?(W+nqQ(7OR1NjpR1IEh50v$s^KydRi8LXoFcf zZ$fxG+%m8Q%!v$wutNrCcdNv42P>%ZyPjFQ?dizYw5a3R&tbxuD<=w)$38xxhtpvV zu6~-+CYRwfJ;9j8C2n@^S2K9)Kn~M7V36NDV%FdFz(Y|ilgln&oqCVtZ{E_4w)yxU z{6{@I(hTe@_lPJg+Z*iW7(e6-pZ~=~{?m(Y6W0UP51naL)p1A4^JiQYttC%9$wZZ! zbXzG^q|%#I>!eZ+s}hbvgNF^~?(n7{UQPFf^WYIVsn5J!4Rg4o4ZxF~&-JT(xnsJB ztZn`Ex>2PwulG+#^u&}U+OhP+=reeg?#Ydmk#VxH{pVQ5&5?32kbcbO82(*opPEua zE-;SK@@5&~8<)nY_`EiK;g!WJ7dKC>4TI%Ms5f&A1q&9A^5-SL-8{;^Fftd(`B-*` ztV;!&!s7=r^_ASf!%b2l=hbG}DvexLfI%4RccVMR8A`sMNSw7m?9DsR%QP1F6{rsJ z{jP-@%1>!RD=l=w06XX~;c)}rl%rU%a)+cL3~r#rzQ#Mr9eC(q7gG%NT(h%T+9XV{NrOa?hnBMhI=YRsM++ zHTWA784Z-;FEOUF66u2kAXR|#{m|KU;aow;ltfQFhWXOwRfS}BX1BBC)N()^)C0!E zTzHm@}O$IlJOT3iF{Bf ziA3*S4f)QML#*om*5x}C(A3&rNh`Qd+u4-yoGj3FHO%k82MC$id>E}-cp_2aEiq<> z(B@Q6F7;e^32asK9>aC#?7a)Ce3iv5AJ|U4NW6vRM~OL<3ghcvZHFsVfN2Mbm0h+!cSopQC)JD-#1`YV z!a?S}eAwALe|`)UrvwuZ7N?{QJjak=X*6G&f=cqs>6l=2i;@y^)V-u}MrE~T)0*x6PbBoExia!N4S=@} z#$=s*Cr~Zlvh!M12>hivHYHP|@j9aONUorNoaKaK+Vq@^o#>`f7q zme4FVf($>iennl~xkR}tkn(TpsWQpK*B0N zdlpNQ6|a>QbiWZm>JW?hW4+Sd1C4jfNGowplaynu$CuoKuIGm%*2XlD5sYdQk%mWWiFpdM&s>8-;Uy zb4G4ORmb_==ADU+nLO3_T%K}tOa1(;j73_U97`#s2JYj!`gvm>7hT1Ix8i#n$(DAY;t=+8(b{AF&Dr}_a2*Ecs(gQ%*goTBpo@xRuAe*?4cK!*ST literal 0 HcmV?d00001 diff --git a/src/components/common/NotificationDropdown.tsx b/src/components/common/NotificationDropdown.tsx index 4dbde12d4..845a8ccc3 100644 --- a/src/components/common/NotificationDropdown.tsx +++ b/src/components/common/NotificationDropdown.tsx @@ -17,8 +17,18 @@ export const NotificationDropdown: React.FC = () => { } = useNotification(); const [isOpen, setIsOpen] = useState(false); + const [isPulsing, setIsPulsing] = useState(false); const dropdownRef = useRef(null); + // Trigger pulse animation when unread count changes + useEffect(() => { + if (unreadCount > 0) { + setIsPulsing(true); + const timer = setTimeout(() => setIsPulsing(false), 2000); + return () => clearTimeout(timer); + } + }, [unreadCount]); + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -35,12 +45,14 @@ export const NotificationDropdown: React.FC = () => {
- - - - {user.name} - - + + {/* Profile dropdown */} +
+ + + {isProfileMenuOpen && ( +
+ setIsProfileMenuOpen(false)} + className="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50" + > + + Edit Profile + + +
+ )} +
) : (
@@ -240,7 +297,7 @@ export const Navbar: React.FC = () => { onClick={() => setIsMenuOpen(false)} > {link.icon} - {link.text} + {link.label} ))} @@ -253,7 +310,7 @@ export const Navbar: React.FC = () => { onClick={() => setIsMenuOpen(false)} > {link.icon} - {link.text} + {link.label} ))} diff --git a/src/context/NotificationContext.tsx b/src/context/NotificationContext.tsx index c6f5665f5..c754ce2e2 100644 --- a/src/context/NotificationContext.tsx +++ b/src/context/NotificationContext.tsx @@ -1,7 +1,9 @@ import React, { createContext, useContext, useState, useEffect, useCallback } from "react"; import axios from "axios"; import { useAuth } from "./AuthContext"; +import { useSocket } from "./SocketContext"; import toast from "react-hot-toast"; +import { playNotificationSound } from "../utils/notificationSound"; interface Notification { _id: string; @@ -31,6 +33,7 @@ const NotificationContext = createContext(u export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { user } = useAuth(); + const { socket } = useSocket(); const [notifications, setNotifications] = useState([]); const [isLoading, setIsLoading] = useState(true); const URL = import.meta.env.VITE_BACKEND_URL; @@ -66,6 +69,49 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ } }, [user, fetchNotifications]); + // Real-time notification listener + useEffect(() => { + if (!socket) return; + + const handleNewNotification = (notification: Notification) => { + console.log('🔔 Real-time notification received:', notification); + + // Add notification to the top of the list + setNotifications(prev => [notification, ...prev]); + + // Show toast notification + toast( +
+
+

{notification.message}

+ {notification.sender && ( +

+ From: {notification.sender.name} +

+ )} +
+
, + { + icon: '🔔', + duration: 5000, + position: 'top-right', + style: { + maxWidth: '400px', + }, + } + ); + + // Play notification sound (optional) + playNotificationSound(); + }; + + socket.on('new-notification', handleNewNotification); + + return () => { + socket.off('new-notification', handleNewNotification); + }; + }, [socket]); + const markAsRead = async (id: string) => { // Optimistic update setNotifications(prev => diff --git a/src/context/SocketContext.tsx b/src/context/SocketContext.tsx index 00f062c87..286ca6f40 100644 --- a/src/context/SocketContext.tsx +++ b/src/context/SocketContext.tsx @@ -18,24 +18,27 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ useEffect(() => { if (user) { // connect socket after login - // const s = io(import.meta.env.VITE_BACKEND_URL, { - // withCredentials: true, - // transports: ["websocket", "polling"], - // }); + const s = io(import.meta.env.VITE_BACKEND_URL, { + withCredentials: true, + transports: ["websocket", "polling"], + }); - // s.on("connect", () => { - // console.log("Connected to socket:", s.id); - // // tell backend this userId is online - // s.emit("join", user.userId); - // }); + s.on("connect", () => { + console.log("Connected to socket:", s.id); + // tell backend this userId is online + s.emit("join", user.userId); + }); - // setSocket(s); + s.on("disconnect", () => { + console.log("Socket disconnected"); + }); + + setSocket(s); // cleanup on unmount or logout return () => { - // s.disconnect(); + s.disconnect(); setSocket(null); - console.log(" Socket disconnected"); }; } }, [user]); diff --git a/src/utils/notificationSound.ts b/src/utils/notificationSound.ts new file mode 100644 index 000000000..9cd560948 --- /dev/null +++ b/src/utils/notificationSound.ts @@ -0,0 +1,44 @@ +/** + * Play notification sound + * Uses a simple beep sound generated via Web Audio API if audio file doesn't exist + */ +export const playNotificationSound = () => { + try { + // Try to play custom notification sound from public folder + const audio = new Audio('/notification.mp3'); + audio.volume = 0.5; + audio.play().catch(() => { + // If custom sound fails, use Web Audio API to generate a simple beep + playBeep(); + }); + } catch (error) { + // Fallback to Web Audio API beep + playBeep(); + } +}; + +/** + * Generate a simple beep sound using Web Audio API + */ +const playBeep = () => { + try { + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = 800; // Frequency in Hz + oscillator.type = 'sine'; + + gainNode.gain.setValueAtTime(0.3, audioContext.currentTime); + gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2); + + oscillator.start(audioContext.currentTime); + oscillator.stop(audioContext.currentTime + 0.2); + } catch (error) { + // Silently fail if Web Audio API is not supported + console.log('Notification sound not supported'); + } +}; From 07768b0b0f7f3210c6fda9bb51b35780f9758201 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Sat, 24 Jan 2026 21:29:37 +0500 Subject: [PATCH 2/4] added socket notificationsa --- src/components/layout/Navbar.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index 918f6ef32..f095f0992 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -15,6 +15,7 @@ import { ClipboardCheck, Ban, ChevronDown, + Home, } from "lucide-react"; import { useAuth } from "../../context/AuthContext"; import { Avatar } from "../ui/Avatar"; @@ -117,11 +118,7 @@ export const Navbar: React.FC = () => { navLinks = [ { icon: - user?.role === "entrepreneur" ? ( - - ) : ( - - ), + , label: "Dashboard", path: dashboardRoute, }, From 21b64dcaa1a287384da70ff6ef1f2dba5379dc34 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Sun, 25 Jan 2026 18:21:53 +0500 Subject: [PATCH 3/4] industries --- dev-dist/sw.js | 2 +- .../entrepreneur/EntrepreneurCard.tsx | 15 +- .../settings/EntrepreneurSettings.tsx | 210 ++++++++++++++++- src/components/settings/EntrepreneurSetup.tsx | 220 ++++++++++++++++-- src/components/settings/InvestorSettings.tsx | 157 +++++++++---- src/components/settings/InvestorSetup.tsx | 123 ++++++++-- src/pages/admin/UserApprovals.tsx | 31 ++- src/pages/dashboard/InvestorDashboard.tsx | 161 ++++++++++--- src/pages/entrepreneurs/EntrepreneursPage.tsx | 31 ++- src/pages/profile/EntrepreneurProfile.tsx | 53 +++-- src/types/index.ts | 2 +- 11 files changed, 864 insertions(+), 141 deletions(-) diff --git a/dev-dist/sw.js b/dev-dist/sw.js index a4d1b1bda..d5961fc2b 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.n93b853l8n" + "revision": "0.ng56cf4st8c" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/entrepreneur/EntrepreneurCard.tsx b/src/components/entrepreneur/EntrepreneurCard.tsx index 168b4153d..9b43b2c99 100644 --- a/src/components/entrepreneur/EntrepreneurCard.tsx +++ b/src/components/entrepreneur/EntrepreneurCard.tsx @@ -55,7 +55,20 @@ export const EntrepreneurCard: React.FC = ({

{entrepreneur.startupName}

- {entrepreneur.industry} + {(() => { + const inds = Array.isArray(entrepreneur.industry) + ? (entrepreneur.industry as string[]) + : (typeof entrepreneur.industry === 'string' && entrepreneur.industry.trim()) + ? [entrepreneur.industry as string] + : []; + return inds.length > 0 ? ( + inds.map((ind, idx) => ( + {ind} + )) + ) : ( + -- + ); + })()} {entrepreneur.teamSize} Founded {entrepreneur.foundedYear}
diff --git a/src/components/settings/EntrepreneurSettings.tsx b/src/components/settings/EntrepreneurSettings.tsx index 78146dec7..e6d256cba 100644 --- a/src/components/settings/EntrepreneurSettings.tsx +++ b/src/components/settings/EntrepreneurSettings.tsx @@ -10,6 +10,9 @@ import { Entrepreneur, UserRole } from "../../types"; import { useAuth } from "../../context/AuthContext"; import { useLocation, useNavigate } from "react-router-dom"; import { Card, CardHeader } from "../ui/Card"; +import { X, Plus, ChevronDown, Check, Loader2 } from "lucide-react"; +import axios from "axios"; +import toast from "react-hot-toast"; type User = { name: string; @@ -24,6 +27,10 @@ export const EntrepreneurSettings: React.FC = () => { const navigate = useNavigate(); const checkIfSettingPage = location.pathname === "/settings" ? true : false; const [entrepreneur, setEnterpreneur] = useState(); + const [industries, setIndustries] = useState<{ _id: string; name: string; isCustom: boolean; }[]>([]); + const [showIndustryDropdown, setShowIndustryDropdown] = useState(false); + const [customIndustry, setCustomIndustry] = useState(""); + const [isAddingCustom, setIsAddingCustom] = useState(false); useEffect(() => { const fetchEntrepreneur = async () => { @@ -35,6 +42,19 @@ export const EntrepreneurSettings: React.FC = () => { fetchEntrepreneur(); }, [user]); + useEffect(() => { + const fetchIndustries = async () => { + try { + const response = await axios.get(`${import.meta.env.VITE_BACKEND_URL}/industry/get-all`); + setIndustries(response.data); + } catch (error) { + console.error("Failed to fetch industries:", error); + toast.error("Failed to load industries"); + } + }; + fetchIndustries(); + }, []); + const initialData = useMemo( () => ({ userId: entrepreneur?.userId, @@ -170,12 +190,190 @@ export const EntrepreneurSettings: React.FC = () => { onChange={handleUserChange} /> - +
+ +
+ {/* Selected Industries */} +
+ {(formData.industry && formData.industry.length > 0) ? formData.industry.map((ind, idx) => ( + + {ind} + + + )) : ( + No industries selected + )} +
+ + {/* Dropdown Button */} + + + {/* Dropdown Menu */} + {showIndustryDropdown && ( +
+ {/* Custom Industry Input */} +
+
+ setCustomIndustry(e.target.value)} + placeholder="Add custom industry..." + className="flex-1 px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm" + onKeyPress={async (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const trimmed = customIndustry.trim(); + if (!trimmed) return; + + const existsInList = industries.some(ind => ind.name.toLowerCase() === trimmed.toLowerCase()); + const alreadySelected = formData.industry?.some(ind => ind.toLowerCase() === trimmed.toLowerCase()); + + if (alreadySelected) { + toast.error('Industry already selected'); + return; + } + + if (!existsInList) { + try { + setIsAddingCustom(true); + const response = await axios.post(`${import.meta.env.VITE_BACKEND_URL}/industry/add-custom`, { + name: trimmed, + userId: user?.userId + }); + setIndustries(prev => [...prev, response.data]); + toast.success('Custom industry added'); + } catch (error: any) { + if (error.response?.data?.message === 'Industry already exists') { + toast.error('Industry already exists in database'); + } else { + toast.error('Failed to add custom industry'); + } + console.error(error); + } finally { + setIsAddingCustom(false); + } + } + + const currentIndustries = formData.industry || []; + setFormData(prev => ({ + ...prev, + industry: [...currentIndustries, trimmed] + })); + setCustomIndustry(''); + } + }} + /> + +
+
+ + {/* Industry Options */} +
+ {industries.map((industry) => { + const isSelected = formData.industry?.includes(industry.name); + return ( + + ); + })} +
+
+ )} +
+
{ const [entrepreneur, setEnterpreneur] = useState(); const isEditMode = checkIfSettingPage && entrepreneur; const [isSubmitting, setIsSubmitting] = useState(false); + const [industries, setIndustries] = useState<{ _id: string; name: string; isCustom: boolean; }[]>([]); + const [showIndustryDropdown, setShowIndustryDropdown] = useState(false); + const [customIndustry, setCustomIndustry] = useState(""); + const [isAddingCustom, setIsAddingCustom] = useState(false); useEffect(() => { const fetchEntrepreneur = async () => { @@ -52,6 +60,19 @@ export const EntrepreneurSetup: React.FC = () => { fetchEntrepreneur(); }, [user]); + useEffect(() => { + const fetchIndustries = async () => { + try { + const response = await axios.get(`${import.meta.env.VITE_BACKEND_URL}/industry/get-all`); + setIndustries(response.data); + } catch (error) { + console.error("Failed to fetch industries:", error); + toast.error("Failed to load industries"); + } + }; + fetchIndustries(); + }, []); + const initialData = useMemo( () => ({ userId: entrepreneur?.userId, @@ -241,21 +262,194 @@ export const EntrepreneurSetup: React.FC = () => {
-
+
-
- -
+
+ {/* Selected Industries */} +
+ {(formData.industry && formData.industry.length > 0) ? formData.industry.map((ind, idx) => ( + + {ind} + + + )) : ( + No industries selected + )} +
+ + {/* Dropdown Button */} + + + {/* Dropdown Menu */} + {showIndustryDropdown && ( +
+ {/* Custom Industry Input */} +
+
+ setCustomIndustry(e.target.value)} + placeholder="Add custom industry..." + className="flex-1 px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent text-sm" + onKeyPress={async (e) => { + if (e.key === 'Enter' && customIndustry.trim()) { + e.preventDefault(); + const trimmed = customIndustry.trim(); + // Check if already exists in dropdown + const existsInList = industries.some(ind => ind.name.toLowerCase() === trimmed.toLowerCase()); + // Check if already selected + const alreadySelected = formData.industry?.some(ind => ind.toLowerCase() === trimmed.toLowerCase()); + + if (alreadySelected) { + toast.error('Industry already selected'); + return; + } + + if (!existsInList) { + // Add to database + try { + setIsAddingCustom(true); + const response = await axios.post(`${import.meta.env.VITE_BACKEND_URL}/industry/add-custom`, { + name: trimmed, + userId: user?.userId + }); + // Add to local industries list + setIndustries(prev => [...prev, response.data]); + toast.success('Custom industry added'); + } catch (error: any) { + if (error.response?.data?.message === 'Industry already exists') { + toast.error('Industry already exists in database'); + } else { + toast.error('Failed to add custom industry'); + } + console.error(error); + } finally { + setIsAddingCustom(false); + } + } + + // Add to selected + const currentIndustries = formData.industry || []; + setFormData(prev => ({ + ...prev, + industry: [...currentIndustries, trimmed] + })); + setCustomIndustry(''); + } + }} + /> + +
+
+ + {/* Industry Options */} +
+ {industries.map((industry) => { + const isSelected = formData.industry?.includes(industry.name); + return ( + + ); + })} +
+
+ )}
diff --git a/src/components/settings/InvestorSettings.tsx b/src/components/settings/InvestorSettings.tsx index 9129a91dc..aea33f042 100644 --- a/src/components/settings/InvestorSettings.tsx +++ b/src/components/settings/InvestorSettings.tsx @@ -9,9 +9,11 @@ import { } from "../../data/users"; import { Button } from "../ui/Button"; import { Input } from "../ui/Input"; -import { Eraser } from "lucide-react"; +import { Eraser, ChevronDown, X, Check, Search } from "lucide-react"; import { useLocation, useNavigate } from "react-router-dom"; import { Card, CardHeader } from "../ui/Card"; +import axios from "axios"; +import toast from "react-hot-toast"; type User = { name: string; @@ -27,6 +29,9 @@ export const InvestorSettings: React.FC = () => { const { user, register } = useAuth(); const [investor, setInvestor] = useState(); + const [industries, setIndustries] = useState<{ _id: string; name: string; isCustom: boolean }[]>([]); + const [showIndustryDropdown, setShowIndustryDropdown] = useState(false); + const [industryQuery, setIndustryQuery] = useState(""); const initialInvestorData = useMemo( () => ({ userId: investor?.userId || user?.userId, @@ -56,6 +61,20 @@ export const InvestorSettings: React.FC = () => { fetchInvestors(); }, [user]); + // Fetch industries from backend + useEffect(() => { + const fetchIndustries = async () => { + try { + const res = await axios.get(`${import.meta.env.VITE_BACKEND_URL}/industry/get-all`); + setIndustries(res.data || []); + } catch (error) { + console.error("Failed to load industries", error); + toast.error("Failed to load industries"); + } + }; + fetchIndustries(); + }, []); + useEffect(() => { setInvestorFormData(initialInvestorData); }, [initialInvestorData]); @@ -167,50 +186,104 @@ export const InvestorSettings: React.FC = () => { onSubmit={handleInvestorSubmit} className="gap-5 flex flex-col text-sm justify-center mt-5" > -
- -
)} - {user.details.industry && ( -
-

Industry

-

{user.details.industry}

-
- )} + {(() => { + const inds = Array.isArray(user.details.industry) + ? user.details.industry + : (typeof user.details.industry === "string" && user.details.industry.trim()) + ? [user.details.industry] + : []; + if (inds.length === 0) return null; + return ( +
+

Industries

+
+ {inds.map((ind, idx) => ( + + {ind} + + ))} +
+
+ ); + })()} {user.details.fundingNeeded && (

Funding Needed

diff --git a/src/pages/dashboard/InvestorDashboard.tsx b/src/pages/dashboard/InvestorDashboard.tsx index 4f6786560..d622a89ff 100644 --- a/src/pages/dashboard/InvestorDashboard.tsx +++ b/src/pages/dashboard/InvestorDashboard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -import { Users, PieChart, Filter, Search, PlusCircle } from "lucide-react"; +import { Users, PieChart, Filter, Search, PlusCircle, ChevronDown, X, Check } from "lucide-react"; import { Button } from "../../components/ui/Button"; import { Card, CardBody, CardHeader } from "../../components/ui/Card"; import { Input } from "../../components/ui/Input"; @@ -10,6 +10,7 @@ import { useAuth } from "../../context/AuthContext"; import { getRequestsFromInvestor } from "../../data/collaborationRequests"; import { getEnterprenuerFromDb } from "../../data/users"; import { CollaborationRequest, Entrepreneur } from "../../types"; +import axios from "axios"; export const InvestorDashboard: React.FC = () => { const { user } = useAuth(); @@ -17,6 +18,9 @@ export const InvestorDashboard: React.FC = () => { const [sentRequests, setSentRequests] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [selectedIndustries, setSelectedIndustries] = useState([]); + const [industries, setIndustries] = useState([]); + const [showFilterDropdown, setShowFilterDropdown] = useState(false); + const [filterQuery, setFilterQuery] = useState(""); if (!user) return null; // Get collaboration requests sent by this investor @@ -28,14 +32,26 @@ export const InvestorDashboard: React.FC = () => { if (user) { const entrepreneurs = await getEnterprenuerFromDb(); setEnterprenuers(entrepreneurs); - entrepreneurs.map((e) => { - industries.push(e.industry); - }); } }; fetchData(); }, []); + // Fetch industries from backend (all available in DB) + useEffect(() => { + const fetchIndustries = async () => { + try { + const res = await axios.get(`${import.meta.env.VITE_BACKEND_URL}/industry/get-all`); + const names = Array.isArray(res.data) ? res.data.map((i: any) => i.name) : []; + // Deduplicate and sort + setIndustries(Array.from(new Set(names)).sort((a, b) => a.localeCompare(b))); + } catch (error) { + console.error("Failed to fetch industries", error); + } + }; + fetchIndustries(); + }, []); + useEffect(() => { const fetchData = async () => { const requests = await getRequestsFromInvestor(user?.userId); @@ -46,6 +62,12 @@ export const InvestorDashboard: React.FC = () => { // Filter entrepreneurs based on search and industry filters const filteredEntrepreneurs = entrepreneurs.filter((entrepreneur) => { + const indList = Array.isArray(entrepreneur?.industry) + ? (entrepreneur.industry as string[]) + : (typeof entrepreneur?.industry === "string" && entrepreneur.industry + ? [entrepreneur.industry as string] + : []); + // Search filter const matchesSearch = searchQuery === "" || @@ -53,9 +75,7 @@ export const InvestorDashboard: React.FC = () => { entrepreneur?.startupName ?.toLowerCase() .includes(searchQuery.toLowerCase()) || - entrepreneur?.industry - ?.toLowerCase() - .includes(searchQuery.toLowerCase()) || + indList.some((ind) => ind.toLowerCase().includes(searchQuery.toLowerCase())) || entrepreneur?.pitchSummary ?.toLowerCase() .includes(searchQuery.toLowerCase()); @@ -63,13 +83,10 @@ export const InvestorDashboard: React.FC = () => { // Industry filter const matchesIndustry = selectedIndustries.length === 0 || - selectedIndustries.includes(entrepreneur?.industry); + indList.some((ind) => selectedIndustries.includes(ind)); return matchesSearch && matchesIndustry; }); - const industries = entrepreneurs - ? [...new Set(entrepreneurs.map((enter) => enter.industry))] - : []; // Get unique industries for filter @@ -112,26 +129,110 @@ export const InvestorDashboard: React.FC = () => {
-
- - - Filter by: - - -
- {industries.map((industry) => ( - toggleIndustry(industry)} - > - {industry} - - ))} +
+
+ + Filter by: +
+ + {/* Selected preview chips */} + {selectedIndustries.length > 0 && ( +
+ {selectedIndustries.map((ind) => ( + + {ind} + + + ))} + +
+ )} + + {/* Dropdown popover */} + {showFilterDropdown && ( +
+
+ + setFilterQuery(e.target.value)} + className="w-full px-2 py-1.5 bg-gray-50 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+
+ {industries + .filter((name) => name.toLowerCase().includes(filterQuery.toLowerCase())) + .map((name) => { + const isSelected = selectedIndustries.includes(name); + return ( + + ); + })} +
+
+ +
+ + +
+
+
+ )}
diff --git a/src/pages/entrepreneurs/EntrepreneursPage.tsx b/src/pages/entrepreneurs/EntrepreneursPage.tsx index 6c81d9c45..b8e7f292b 100644 --- a/src/pages/entrepreneurs/EntrepreneursPage.tsx +++ b/src/pages/entrepreneurs/EntrepreneursPage.tsx @@ -6,6 +6,7 @@ import { EntrepreneurCard } from '../../components/entrepreneur/EntrepreneurCard import { useAuth } from '../../context/AuthContext'; import { getEnterprenuerFromDb } from '../../data/users'; import { Entrepreneur } from '../../types'; +import axios from 'axios'; export const EntrepreneursPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); @@ -26,7 +27,21 @@ export const EntrepreneursPage: React.FC = () => { fetchData(); }, [user]); - const allIndustries = Array.from(new Set(entrepreneurs.map(e => e.industry || 'Other').filter(Boolean))); + // Industries from backend (DB) + const [industries, setIndustries] = useState([]); + + useEffect(() => { + const fetchIndustries = async () => { + try { + const res = await axios.get(`${import.meta.env.VITE_BACKEND_URL}/industry/get-all`); + const names = Array.isArray(res.data) ? res.data.map((i: any) => i.name) : []; + setIndustries(Array.from(new Set(names)).sort((a, b) => a.localeCompare(b))); + } catch (error) { + console.error('Failed to fetch industries', error); + } + }; + fetchIndustries(); + }, []); // Filter out undefined/null locations and get unique ones const allLocations = Array.from(new Set(entrepreneurs.map(e => e.location).filter(Boolean))); @@ -34,14 +49,20 @@ export const EntrepreneursPage: React.FC = () => { // Filter entrepreneurs based on search and filters const filteredEntrepreneurs = entrepreneurs.filter(entrepreneur => { + const indList = Array.isArray(entrepreneur.industry) + ? (entrepreneur.industry as string[]) + : (typeof entrepreneur.industry === 'string' && entrepreneur.industry) + ? [entrepreneur.industry as string] + : []; + const matchesSearch = searchQuery === '' || entrepreneur.name.toLowerCase().includes(searchQuery.toLowerCase()) || (entrepreneur.startupName || '').toLowerCase().includes(searchQuery.toLowerCase()) || - (entrepreneur.industry || '').toLowerCase().includes(searchQuery.toLowerCase()) || + indList.some(ind => ind.toLowerCase().includes(searchQuery.toLowerCase())) || (entrepreneur.pitchSummary || '').toLowerCase().includes(searchQuery.toLowerCase()); const matchesIndustry = selectedIndustries.length === 0 || - selectedIndustries.includes(entrepreneur.industry || 'Other'); + indList.some(ind => selectedIndustries.includes(ind)); const matchesLocation = selectedLocations.length === 0 || selectedLocations.includes(entrepreneur.location); @@ -106,7 +127,7 @@ export const EntrepreneursPage: React.FC = () => {

Industry

- {allIndustries.map(industry => ( + {industries.map(industry => ( ))} - {allIndustries.length === 0 &&

No industries found.

} + {industries.length === 0 &&

No industries found.

}
diff --git a/src/pages/profile/EntrepreneurProfile.tsx b/src/pages/profile/EntrepreneurProfile.tsx index 944ef8f9f..bb5c5299b 100644 --- a/src/pages/profile/EntrepreneurProfile.tsx +++ b/src/pages/profile/EntrepreneurProfile.tsx @@ -347,7 +347,20 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => {

- {entrepreneur.industry || "--"} + {(() => { + const inds = Array.isArray(entrepreneur.industry) + ? entrepreneur.industry + : (typeof entrepreneur.industry === "string" && entrepreneur.industry.trim()) + ? [entrepreneur.industry] + : []; + return inds.length > 0 ? ( + inds.map((ind, idx) => ( + {ind} + )) + ) : ( + -- + ); + })()} {entrepreneur.location || "--"} @@ -867,19 +880,31 @@ export const EntrepreneurProfile: React.FC = ({ userId }) => { Previous Funding - {entrepreneur.fundingHistory && entrepreneur.fundingHistory.length > 0 ? ( -
- {entrepreneur.fundingHistory.map((fund: any, index: number) => ( -

- ${AmountMeasureWithTags(fund.amount)} {fund.stage} ({fund.year}) -

- ))} -
- ) : ( -

- N/A -

- )} + {(() => { + const list = Array.isArray(entrepreneur.fundingHistory) + ? entrepreneur.fundingHistory + : []; + const sortedLatestThree = list + .slice() + .sort((a: any, b: any) => { + const ta = a?.date ? new Date(a.date).getTime() : (typeof a?.year === "number" ? a.year : 0); + const tb = b?.date ? new Date(b.date).getTime() : (typeof b?.year === "number" ? b.year : 0); + return tb - ta; // Descending (latest first) + }) + .slice(0, 3); + + return sortedLatestThree.length > 0 ? ( +
+ {sortedLatestThree.map((fund: any, index: number) => ( +

+ ${AmountMeasureWithTags(fund.amount)} {fund.stage} ({fund.year ?? (fund.date ? new Date(fund.date).getFullYear() : "")}) +

+ ))} +
+ ) : ( +

N/A

+ ); + })()}
diff --git a/src/types/index.ts b/src/types/index.ts index ef17e077f..d2e4d3232 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,7 +25,7 @@ export interface Entrepreneur extends User { startupName: string | undefined; pitchSummary: string | undefined; fundingNeeded: number | undefined; - industry: string | undefined; + industry: string[] | undefined; foundedYear: number | undefined; teamSize: number | undefined; team?: TeamMember[]; From b8a73c7696eef3f39a3e28003f6421a67c3e19f0 Mon Sep 17 00:00:00 2001 From: mzain4321 Date: Mon, 26 Jan 2026 13:51:26 +0500 Subject: [PATCH 4/4] kyc --- dev-dist/sw.js | 2 +- src/components/DealForm.tsx | 4 +- src/components/DealPaymentModal.tsx | 4 +- src/components/settings/LegalInfoUpload.tsx | 327 ++++++++++++++++++++ src/pages/admin/AdminDealPayments.tsx | 9 +- src/pages/admin/AdminDealsPage.tsx | 52 +++- src/pages/admin/UserApprovals.tsx | 197 ++++++++++++ src/pages/admin/Users.tsx | 258 ++++++++++++++- src/pages/deals/DealsPage.tsx | 10 + src/pages/legal/TermsOfService.tsx | 4 +- src/pages/settings/SettingsPage.tsx | 12 +- 11 files changed, 852 insertions(+), 27 deletions(-) create mode 100644 src/components/settings/LegalInfoUpload.tsx diff --git a/dev-dist/sw.js b/dev-dist/sw.js index d5961fc2b..1df8fbe9e 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-d70286d7'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.ng56cf4st8c" + "revision": "0.otavhr08nak" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/components/DealForm.tsx b/src/components/DealForm.tsx index 9aecd9d3b..d4f53c60f 100644 --- a/src/components/DealForm.tsx +++ b/src/components/DealForm.tsx @@ -184,9 +184,9 @@ export const DealForm: React.FC = ({ /> {formData.investmentAmount && (
-
After 5% Commission
+
After 2% Commission
- ${(Number(formData.investmentAmount) * 0.95).toFixed(2)} + ${(Number(formData.investmentAmount) * 0.98).toFixed(2)}
)} diff --git a/src/components/DealPaymentModal.tsx b/src/components/DealPaymentModal.tsx index 43b93c4b5..3472b1985 100644 --- a/src/components/DealPaymentModal.tsx +++ b/src/components/DealPaymentModal.tsx @@ -143,9 +143,9 @@ export const DealPaymentModal: React.FC = ({ )} {amount > 0 && (
-
After 5% Commission
+
After 2% Commission
- ${(Number(amount) * 0.95).toFixed(2)} + ${(Number(amount) * 0.98).toFixed(2)}
)} diff --git a/src/components/settings/LegalInfoUpload.tsx b/src/components/settings/LegalInfoUpload.tsx new file mode 100644 index 000000000..0c3c31150 --- /dev/null +++ b/src/components/settings/LegalInfoUpload.tsx @@ -0,0 +1,327 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import axios from "axios"; +import { Card, CardBody, CardHeader } from "../ui/Card"; +import { Upload, CheckCircle2, AlertTriangle } from "lucide-react"; +import toast from "react-hot-toast"; +import { useAuth } from "../../context/AuthContext"; + +interface UserDocument { + _id: string; + type: string; + fileUrl: string; + fileName: string; + uploadedAt: string; +} + +const LEGAL_ITEMS = [ + { + type: "Government ID (CNIC/Passport)", + label: "Government ID (CNIC/Passport)", + description: "Upload your CNIC or passport (PDF, JPG, PNG).", + accept: ".pdf,.jpg,.jpeg,.png", + }, + { + type: "Selfie Photo", + label: "Selfie with ID", + description: "Upload a clear selfie holding your ID (JPG, PNG).", + accept: ".jpg,.jpeg,.png", + }, +]; + +export const LegalInfoUpload: React.FC = () => { + const URL = import.meta.env.VITE_BACKEND_URL; + const { user: authUser } = useAuth(); + const token = useMemo(() => localStorage.getItem("token"), []); + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(false); + const [uploadingType, setUploadingType] = useState(null); + const [kycStatus, setKycStatus] = useState<"unsubmitted" | "pending" | "verified" | "rejected">("unsubmitted"); + const [deleting, setDeleting] = useState(false); + const videoRef = useRef(null); + const streamRef = useRef(null); + const [cameraActive, setCameraActive] = useState(false); + const [cameraError, setCameraError] = useState(""); + const [cameraBusyType, setCameraBusyType] = useState(null); + + const deleteDocument = async (id: string) => { + if (!token) return; + await axios.delete(`${URL}/document/${id}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + }; + + const fetchKycData = async () => { + if (!token || !authUser?.userId) return; + try { + setLoading(true); + const [docRes, userRes] = await Promise.all([ + axios.get(`${URL}/document`, { + headers: { Authorization: `Bearer ${token}` }, + }), + axios.get(`${URL}/user/get-user-by-id/${authUser.userId}`, { + headers: { Authorization: `Bearer ${token}` }, + }), + ]); + + const docs = docRes.data.documents || []; + setDocuments(docs); + + const status = userRes.data?.user?.kycStatus?.status || "unsubmitted"; + setKycStatus(status); + + // If rejected, remove previous uploads so the user can re-upload fresh + if (status === "rejected" && docs.length > 0) { + setDeleting(true); + try { + await Promise.all(docs.map((d: UserDocument) => deleteDocument(d._id))); + setDocuments([]); + } catch (err) { + console.error("Failed to clear rejected docs", err); + toast.error("Could not clear rejected documents. Please try again."); + } finally { + setDeleting(false); + } + } + } catch (error) { + console.error("Failed to load legal info", error); + toast.error("Could not load legal info"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchKycData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleUpload = async (file: File | Blob | null, type: string) => { + if (!file) return; + if (!token) { + toast.error("Not authenticated"); + return; + } + + if (kycStatus === "verified") { + toast.error("KYC already verified. No further uploads needed."); + return; + } + + const fileForUpload = file instanceof File ? file : new File([file], "selfie.png", { type: "image/png" }); + + const isImage = fileForUpload.type.startsWith("image/"); + const isPdf = fileForUpload.type === "application/pdf"; + if (!isImage && !isPdf) { + toast.error("Only PDF, JPG, or PNG files are allowed"); + return; + } + + setUploadingType(type); + const formData = new FormData(); + formData.append("file", fileForUpload); + formData.append("type", type); + + try { + await axios.post(`${URL}/document/upload`, formData, { + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "multipart/form-data", + }, + }); + toast.success(`${type} uploaded`); + fetchKycData(); + } catch (error: any) { + console.error(error); + const msg = error?.response?.data?.message || "Upload failed"; + toast.error(msg); + } finally { + setUploadingType(null); + } + }; + + const stopCamera = () => { + streamRef.current?.getTracks().forEach((t) => t.stop()); + streamRef.current = null; + setCameraActive(false); + setCameraBusyType(null); + }; + + const startCamera = async (type: string) => { + setCameraError(""); + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" } }); + if (videoRef.current) { + videoRef.current.srcObject = stream; + await videoRef.current.play(); + } + streamRef.current = stream; + setCameraActive(true); + setCameraBusyType(type); + } catch (err) { + console.error(err); + setCameraError("Camera access denied or unavailable"); + stopCamera(); + } + }; + + const capturePhoto = async (type: string) => { + if (!cameraActive || cameraBusyType !== type || !videoRef.current) return; + + const video = videoRef.current; + const canvas = document.createElement("canvas"); + canvas.width = video.videoWidth || 640; + canvas.height = video.videoHeight || 480; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + if (!blob) return; + handleUpload(blob, type); + }, "image/png"); + }; + + useEffect(() => { + return () => { + stopCamera(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const displayStatus = + kycStatus === "unsubmitted" && documents.length === 0 ? "pending" : kycStatus; + + const isVerified = kycStatus === "verified"; + + return ( +
+ + +
+
+

Legal Verification

+

+ Upload your government ID and selfie to verify your account. Files are stored securely with your profile. +

+
+
+ + {`Status: ${displayStatus}`} + + {loading && Loading...} +
+
+
+ + {LEGAL_ITEMS.map((item) => { + const existing = documents.find((doc) => doc.type === item.type); + const inputId = `legal-upload-${item.type.replace(/\s+/g, "-")}`; + return ( +
+
+
+ +
+
+

{item.label}

+

{item.description}

+
+
+ + {existing ? ( +
+ +
+
{existing.fileName}
+
Uploaded {new Date(existing.uploadedAt).toLocaleDateString()}
+
+
+ ) : ( +
+ + No file uploaded yet +
+ )} + + {!isVerified && ( +
+ + handleUpload(e.target.files?.[0] || null, item.type)} + disabled={uploadingType === item.type || deleting} + /> + Allowed: {item.accept.replace(/\./g, "").replace(/,/g, ", ")} +
+ )} + + {item.type === "Selfie Photo" && !isVerified && ( +
+ {cameraError &&
{cameraError}
} +
+ {!cameraActive ? ( + + ) : ( + <> + + + + )} + {cameraActive && Camera ready} +
+ {cameraActive && ( +
+
+ )} +
+ )} +
+ ); + })} +
+
+
+ ); +}; diff --git a/src/pages/admin/AdminDealPayments.tsx b/src/pages/admin/AdminDealPayments.tsx index 25576b610..3275fa0c5 100644 --- a/src/pages/admin/AdminDealPayments.tsx +++ b/src/pages/admin/AdminDealPayments.tsx @@ -45,7 +45,14 @@ export const AdminDealPayments: React.FC = () => { setConfirmModal({ isOpen: false, transactionId: null }); fetchTransactions(); } catch (error: any) { - toast.error(error.response?.data?.message || "Failed to release funds"); + const msg = error.response?.data?.message || "Failed to release funds"; + const isKycBlock = error.response?.status === 400 && msg.toLowerCase().includes("kyc"); + + if (isKycBlock) { + toast.error("Entrepreneur KYC not verified. They were notified to complete it."); + } else { + toast.error(msg); + } setConfirmModal({ isOpen: false, transactionId: null }); } }; diff --git a/src/pages/admin/AdminDealsPage.tsx b/src/pages/admin/AdminDealsPage.tsx index e1b03f863..272a98e46 100644 --- a/src/pages/admin/AdminDealsPage.tsx +++ b/src/pages/admin/AdminDealsPage.tsx @@ -12,6 +12,8 @@ export const AdminDealsPage: React.FC = () => { const [deals, setDeals] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); const [selectedDeal, setSelectedDeal] = useState(null); const [isViewModalOpen, setIsViewModalOpen] = useState(false); @@ -37,13 +39,25 @@ export const AdminDealsPage: React.FC = () => { setIsViewModalOpen(true); }; + const startDateObj = startDate ? new Date(startDate) : null; + const endDateObj = endDate ? new Date(endDate) : null; + if (endDateObj) endDateObj.setHours(23, 59, 59, 999); + const filteredDeals = deals.filter((deal) => { const term = searchTerm.toLowerCase(); + const createdAt = deal.createdAt ? new Date(deal.createdAt) : null; + + const matchesDate = !createdAt + ? !(startDateObj || endDateObj) + : (!startDateObj || createdAt >= startDateObj) && (!endDateObj || createdAt <= endDateObj); + return ( - deal.investorId?.name?.toLowerCase().includes(term) || - deal.entrepreneurId?.name?.toLowerCase().includes(term) || - deal.entrepreneurId?.startupName?.toLowerCase().includes(term) || - deal.status?.toLowerCase().includes(term) + ( + deal.investorId?.name?.toLowerCase().includes(term) || + deal.entrepreneurId?.name?.toLowerCase().includes(term) || + deal.entrepreneurId?.startupName?.toLowerCase().includes(term) || + deal.status?.toLowerCase().includes(term) + ) && matchesDate ); }); @@ -53,14 +67,30 @@ export const AdminDealsPage: React.FC = () => {

All Deal Records

-
- +
+
+ + setSearchTerm(e.target.value)} + /> +
+ setStartDate(e.target.value)} + aria-label="Start date" + /> setSearchTerm(e.target.value)} + type="date" + className="px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + value={endDate} + onChange={(e) => setEndDate(e.target.value)} + aria-label="End date" />
diff --git a/src/pages/admin/UserApprovals.tsx b/src/pages/admin/UserApprovals.tsx index 760fe48fc..f134b8c6c 100644 --- a/src/pages/admin/UserApprovals.tsx +++ b/src/pages/admin/UserApprovals.tsx @@ -28,6 +28,13 @@ interface PendingUser { approvalStatus?: string; approvalDate?: string; rejectionReason?: string; + kycStatus?: { + status?: "unsubmitted" | "pending" | "verified" | "rejected"; + note?: string; + reviewedAt?: string; + reviewedBy?: string; + }; + avatarUrl?: string; details?: { startupName?: string; industry?: string[]; @@ -72,6 +79,11 @@ export const UserApprovals: React.FC = () => { const [showRejectModal, setShowRejectModal] = useState(null); const [selectedUser, setSelectedUser] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); + const [showLegalModal, setShowLegalModal] = useState(false); + const [legalDocs, setLegalDocs] = useState([]); + const [legalLoading, setLegalLoading] = useState(false); + const [kycNote, setKycNote] = useState(""); + const [kycUpdating, setKycUpdating] = useState(false); // Add loading states for approval and rejection buttons const [approvingUsers, setApprovingUsers] = useState<{ [key: string]: boolean }>({}); const [rejectingUsers, setRejectingUsers] = useState<{ [key: string]: boolean }>({}); @@ -200,6 +212,50 @@ export const UserApprovals: React.FC = () => { } }; + // Legal docs / KYC helpers + const fetchLegalDocs = async (userId: string) => { + try { + setLegalLoading(true); + const res = await axios.get(`${URL}/admin/kyc/documents/${userId}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setLegalDocs(res.data.documents || []); + } catch (error) { + console.error(error); + toast.error("Failed to load legal documents"); + setLegalDocs([]); + } finally { + setLegalLoading(false); + } + }; + + const openLegalModal = async (user: PendingUser) => { + setSelectedUser(user); + setShowLegalModal(true); + setKycNote(user.kycStatus?.note || ""); + await fetchLegalDocs(user._id); + }; + + const updateKycStatus = async (status: "verified" | "rejected" | "pending" | "unsubmitted") => { + if (!selectedUser) return; + try { + setKycUpdating(true); + await axios.post( + `${URL}/admin/kyc/status/${selectedUser._id}`, + { status, note: kycNote }, + { headers: { Authorization: `Bearer ${token}` } } + ); + toast.success(`KYC marked as ${status}`); + setShowLegalModal(false); + setKycUpdating(false); + fetchApprovalData(); + } catch (error) { + console.error(error); + toast.error("Failed to update KYC status"); + setKycUpdating(false); + } + }; + // Delete rejected user const handleDeleteRejected = (userId: string) => { setDeleteModal({ show: true, userId }); @@ -513,6 +569,33 @@ export const UserApprovals: React.FC = () => {
); + +
+ + {user.kycStatus?.status && ( + + KYC: {user.kycStatus.status} + + )} +
}; const DetailModal = ({ user }: { user: PendingUser | null }) => { @@ -606,6 +689,119 @@ export const UserApprovals: React.FC = () => { ); }; + const LegalDocsModal = ({ user }: { user: PendingUser }) => { + const idDoc = legalDocs.find((d) => d.type === "Government ID (CNIC/Passport)"); + const selfieDoc = legalDocs.find((d) => d.type === "Selfie Photo"); + const fullUrl = (doc?: any) => { + if (!doc) return "#"; + return doc.fileUrl?.startsWith("http") ? doc.fileUrl : `${URL}${doc.fileUrl}`; + }; + + return ( +
{ + if (e.target === e.currentTarget) setShowLegalModal(false); + }} + > + + +
+

Legal Documents

+

Review and verify identity documents

+
+ +
+ +
+
+ +
+

{user.name}

+

{user.email}

+

KYC Status: {user.kycStatus?.status || "unsubmitted"}

+
+
+ +
+ {[{ title: "Government ID (CNIC/Passport)", doc: idDoc }, { title: "Selfie Photo", doc: selfieDoc }].map(({ title, doc }) => ( +
+
+
+

{title}

+

{doc ? "Uploaded" : "Not provided"}

+
+ {doc && ( + + View + + )} +
+ {doc ? ( +
+

{doc.fileName}

+

Uploaded {new Date(doc.uploadedAt).toLocaleString()}

+
+ ) : ( +
No file uploaded.
+ )} +
+ ))} +
+ +
+ +