From ab754f13f2561cdf45f8fca8afd2877071f4d73e Mon Sep 17 00:00:00 2001 From: jullia02 Date: Sat, 19 Apr 2025 11:03:41 +0300 Subject: [PATCH 01/22] changed manifest file --- frontend/public/logo192.png | Bin 5347 -> 890 bytes frontend/public/logo512.png | Bin 9664 -> 9924 bytes frontend/public/manifest.json | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/public/logo192.png b/frontend/public/logo192.png index fc44b0a3796c0e0a64c3d858ca038bd4570465d9..6988eeac7808c6c449a8b22af74f8ddace2ec8e4 100644 GIT binary patch literal 890 zcmV-=1BLvFP)Px&G)Y83RCr$Po4d*sK@f$%MZ85O23`@3j77!NKzsuK$R|)l6HUEjf}n`t1DNR> z7>KDT_yD2^KlmdihKizYFU~TKJ=>>yYNmU4XTsg?s&h_Vx@LBE!kS-`HRn&P0LOrr zz+q9iSo@+`yJZT%y}*|aE)^dg%y-)w0{6R{`T6dUA0s#MX4-%~nnwL{yt&pLOC`V- zu+NaZGnG?-C&0x9pE;f;uou8tWAZr#SX9CXfUUX!JArRKkk2K6(!VBk0hA4?`V3tH zDwiJ>u!q2vRCa-VrIMe)uE42^#cH&>$FlNaiRO_az&7AltP%sPk0xJbz(?SS2yp0h z7GaS@9@ub%)Xl3#ewF|^1#mZ1yMT9Um_6&%;Q3t40;&q|6WHDXXnkuN=-3SFOg=_{ zPryL~0Yacv5khtrLH);vngJF9sGvh4sCHV@zWzmt0xTMxP00^k05RidmC$ z4v4|o+uBtVz`eop-lNuY;C$OLqBNeqk3owFzXnbQ4E@(XRw)CNK%NGiQ1ffu1h_qA zerpt9%z&{3#vV{9Fg8K)LFF>c0m?Bro&u@Ipykiw7E_;S&^izA!$*5=*( z!aX}Xmknk=j)SkrG~K$ne}6w%2DchKw%Zmw0Im)W*VbW>ymvBcU|7b+_!c;!YN@ES zXL>|_RSjlb_c`U?Zi)bPPOdk&vGaIFqIK7Irc>jT!%$dLEBTQ8R9|wd3008in6lFF3GV-6mLi?MoP_y~}QUnaDCHI#t z7w^m$@6DI)|C8_jrT?q=f8D?0AM?L)Z}xAo^e^W>t$*Y0KlT5=@bBjT9kxb%-KNdk zeOS1tKO#ChhG7%{ApNBzE2ZVNcxbrin#E1TiAw#BlUhXllzhN$qWez5l;h+t^q#Eav8PhR2|T}y5kkflaK`ba-eoE+Z2q@o6P$)=&` z+(8}+-McnNO>e#$Rr{32ngsZIAX>GH??tqgwUuUz6kjns|LjsB37zUEWd|(&O!)DY zQLrq%Y>)Y8G`yYbYCx&aVHi@-vZ3|ebG!f$sTQqMgi0hWRJ^Wc+Ibv!udh_r%2|U) zPi|E^PK?UE!>_4`f`1k4hqqj_$+d!EB_#IYt;f9)fBOumGNyglU(ofY`yHq4Y?B%- zp&G!MRY<~ajTgIHErMe(Z8JG*;D-PJhd@RX@QatggM7+G(Lz8eZ;73)72Hfx5KDOE zkT(m}i2;@X2AT5fW?qVp?@WgN$aT+f_6eo?IsLh;jscNRp|8H}Z9p_UBO^SJXpZew zEK8fz|0Th%(Wr|KZBGTM4yxkA5CFdAj8=QSrT$fKW#tweUFqr0TZ9D~a5lF{)%-tTGMK^2tz(y2v$i%V8XAxIywrZCp=)83p(zIk6@S5AWl|Oa2hF`~~^W zI;KeOSkw1O#TiQ8;U7OPXjZM|KrnN}9arP)m0v$c|L)lF`j_rpG(zW1Qjv$=^|p*f z>)Na{D&>n`jOWMwB^TM}slgTEcjxTlUby89j1)|6ydRfWERn3|7Zd2&e7?!K&5G$x z`5U3uFtn4~SZq|LjFVrz$3iln-+ucY4q$BC{CSm7Xe5c1J<=%Oagztj{ifpaZk_bQ z9Sb-LaQMKp-qJA*bP6DzgE3`}*i1o3GKmo2pn@dj0;He}F=BgINo};6gQF8!n0ULZ zL>kC0nPSFzlcB7p41doao2F7%6IUTi_+!L`MM4o*#Y#0v~WiO8uSeAUNp=vA2KaR&=jNR2iVwG>7t%sG2x_~yXzY)7K& zk3p+O0AFZ1eu^T3s};B%6TpJ6h-Y%B^*zT&SN7C=N;g|#dGIVMSOru3iv^SvO>h4M=t-N1GSLLDqVTcgurco6)3&XpU!FP6Hlrmj}f$ zp95;b)>M~`kxuZF3r~a!rMf4|&1=uMG$;h^g=Kl;H&Np-(pFT9FF@++MMEx3RBsK?AU0fPk-#mdR)Wdkj)`>ZMl#^<80kM87VvsI3r_c@_vX=fdQ`_9-d(xiI z4K;1y1TiPj_RPh*SpDI7U~^QQ?%0&!$Sh#?x_@;ag)P}ZkAik{_WPB4rHyW#%>|Gs zdbhyt=qQPA7`?h2_8T;-E6HI#im9K>au*(j4;kzwMSLgo6u*}-K`$_Gzgu&XE)udQ zmQ72^eZd|vzI)~!20JV-v-T|<4@7ruqrj|o4=JJPlybwMg;M$Ud7>h6g()CT@wXm` zbq=A(t;RJ^{Xxi*Ff~!|3!-l_PS{AyNAU~t{h;(N(PXMEf^R(B+ZVX3 z8y0;0A8hJYp@g+c*`>eTA|3Tgv9U8#BDTO9@a@gVMDxr(fVaEqL1tl?md{v^j8aUv zm&%PX4^|rX|?E4^CkplWWNv*OKM>DxPa z!RJ)U^0-WJMi)Ksc!^ixOtw^egoAZZ2Cg;X7(5xZG7yL_;UJ#yp*ZD-;I^Z9qkP`} zwCTs0*%rIVF1sgLervtnUo&brwz?6?PXRuOCS*JI-WL6GKy7-~yi0giTEMmDs_-UX zo=+nFrW_EfTg>oY72_4Z0*uG>MnXP=c0VpT&*|rvv1iStW;*^={rP1y?Hv+6R6bxFMkxpWkJ>m7Ba{>zc_q zEefC3jsXdyS5??Mz7IET$Kft|EMNJIv7Ny8ZOcKnzf`K5Cd)&`-fTY#W&jnV0l2vt z?Gqhic}l}mCv1yUEy$%DP}4AN;36$=7aNI^*AzV(eYGeJ(Px-j<^gSDp5dBAv2#?; zcMXv#aj>%;MiG^q^$0MSg-(uTl!xm49dH!{X0){Ew7ThWV~Gtj7h%ZD zVN-R-^7Cf0VH!8O)uUHPL2mO2tmE*cecwQv_5CzWeh)ykX8r5Hi`ehYo)d{Jnh&3p z9ndXT$OW51#H5cFKa76c<%nNkP~FU93b5h-|Cb}ScHs@4Q#|}byWg;KDMJ#|l zE=MKD*F@HDBcX@~QJH%56eh~jfPO-uKm}~t7VkHxHT;)4sd+?Wc4* z>CyR*{w@4(gnYRdFq=^(#-ytb^5ESD?x<0Skhb%Pt?npNW1m+Nv`tr9+qN<3H1f<% zZvNEqyK5FgPsQ`QIu9P0x_}wJR~^CotL|n zk?dn;tLRw9jJTur4uWoX6iMm914f0AJfB@C74a;_qRrAP4E7l890P&{v<}>_&GLrW z)klculcg`?zJO~4;BBAa=POU%aN|pmZJn2{hA!d!*lwO%YSIzv8bTJ}=nhC^n}g(ld^rn#kq9Z3)z`k9lvV>y#!F4e{5c$tnr9M{V)0m(Z< z#88vX6-AW7T2UUwW`g<;8I$Jb!R%z@rCcGT)-2k7&x9kZZT66}Ztid~6t0jKb&9mm zpa}LCb`bz`{MzpZR#E*QuBiZXI#<`5qxx=&LMr-UUf~@dRk}YI2hbMsAMWOmDzYtm zjof16D=mc`^B$+_bCG$$@R0t;e?~UkF?7<(vkb70*EQB1rfUWXh$j)R2)+dNAH5%R zEBs^?N;UMdy}V};59Gu#0$q53$}|+q7CIGg_w_WlvE}AdqoS<7DY1LWS9?TrfmcvT zaypmplwn=P4;a8-%l^e?f`OpGb}%(_mFsL&GywhyN(-VROj`4~V~9bGv%UhcA|YW% zs{;nh@aDX11y^HOFXB$a7#Sr3cEtNd4eLm@Y#fc&j)TGvbbMwze zXtekX_wJqxe4NhuW$r}cNy|L{V=t#$%SuWEW)YZTH|!iT79k#?632OFse{+BT_gau zJwQcbH{b}dzKO?^dV&3nTILYlGw{27UJ72ZN){BILd_HV_s$WfI2DC<9LIHFmtyw? zQ;?MuK7g%Ym+4e^W#5}WDLpko%jPOC=aN)3!=8)s#Rnercak&b3ESRX3z{xfKBF8L z5%CGkFmGO@x?_mPGlpEej!3!AMddChabyf~nJNZxx!D&{@xEb!TDyvqSj%Y5@A{}9 zRzoBn0?x}=krh{ok3Nn%e)#~uh;6jpezhA)ySb^b#E>73e*frBFu6IZ^D7Ii&rsiU z%jzygxT-n*joJpY4o&8UXr2s%j^Q{?e-voloX`4DQyEK+DmrZh8A$)iWL#NO9+Y@!sO2f@rI!@jN@>HOA< z?q2l{^%mY*PNx2FoX+A7X3N}(RV$B`g&N=e0uvAvEN1W^{*W?zT1i#fxuw10%~))J zjx#gxoVlXREWZf4hRkgdHx5V_S*;p-y%JtGgQ4}lnA~MBz-AFdxUxU1RIT$`sal|X zPB6sEVRjGbXIP0U+?rT|y5+ev&OMX*5C$n2SBPZr`jqzrmpVrNciR0e*Wm?fK6DY& zl(XQZ60yWXV-|Ps!A{EF;=_z(YAF=T(-MkJXUoX zI{UMQDAV2}Ya?EisdEW;@pE6dt;j0fg5oT2dxCi{wqWJ<)|SR6fxX~5CzblPGr8cb zUBVJ2CQd~3L?7yfTpLNbt)He1D>*KXI^GK%<`bq^cUq$Q@uJifG>p3LU(!H=C)aEL zenk7pVg}0{dKU}&l)Y2Y2eFMdS(JS0}oZUuVaf2+K*YFNGHB`^YGcIpnBlMhO7d4@vV zv(@N}(k#REdul8~fP+^F@ky*wt@~&|(&&meNO>rKDEnB{ykAZ}k>e@lad7to>Ao$B zz<1(L=#J*u4_LB=8w+*{KFK^u00NAmeNN7pr+Pf+N*Zl^dO{LM-hMHyP6N!~`24jd zXYP|Ze;dRXKdF2iJG$U{k=S86l@pytLx}$JFFs8e)*Vi?aVBtGJ3JZUj!~c{(rw5>vuRF$`^p!P8w1B=O!skwkO5yd4_XuG^QVF z`-r5K7(IPSiKQ2|U9+`@Js!g6sfJwAHVd|s?|mnC*q zp|B|z)(8+mxXyxQ{8Pg3F4|tdpgZZSoU4P&9I8)nHo1@)9_9u&NcT^FI)6|hsAZFk zZ+arl&@*>RXBf-OZxhZerOr&dN5LW9@gV=oGFbK*J+m#R-|e6(Loz(;g@T^*oO)0R zN`N=X46b{7yk5FZGr#5&n1!-@j@g02g|X>MOpF3#IjZ_4wg{dX+G9eqS+Es9@6nC7 zD9$NuVJI}6ZlwtUm5cCAiYv0(Yi{%eH+}t)!E^>^KxB5^L~a`4%1~5q6h>d;paC9c zTj0wTCKrhWf+F#5>EgX`sl%POl?oyCq0(w0xoL?L%)|Q7d|Hl92rUYAU#lc**I&^6p=4lNQPa0 znQ|A~i0ip@`B=FW-Q;zh?-wF;Wl5!+q3GXDu-x&}$gUO)NoO7^$BeEIrd~1Dh{Tr` z8s<(Bn@gZ(mkIGnmYh_ehXnq78QL$pNDi)|QcT*|GtS%nz1uKE+E{7jdEBp%h0}%r zD2|KmYGiPa4;md-t_m5YDz#c*oV_FqXd85d@eub?9N61QuYcb3CnVWpM(D-^|CmkL z(F}L&N7qhL2PCq)fRh}XO@U`Yn<?TNGR4L(mF7#4u29{i~@k;pLsgl({YW5`Mo+p=zZn3L*4{JU;++dG9 X@eDJUQo;Ye2mwlRs!y=iIa1bI(2ZChy#_RZ4u7H~>J3 z{Lq2WBL*R1$8x`0uL3|FkQ_F9MCE-OjC^p^GiU5a$YA+dZ28O0b{R#7ccFG3LeILL zOS3=VyK@NWy^~*0F?}mf&q-R=-B^EV{8F7b>*I>2=kzU~tmu6Hfl;m@=X@EfSLE#I z*{;^;*D$~wmKw_&`x@6=SX&@m@uYIBAZB`z`{|JzAkm5c%fF|0#_=9G>&Yc1{1#Os z!}@8w3iu+T#3H>m-4O}s!oNI?p5w)l9= zol7US(LcTc%YqDQ)^TFl!a=-7nE+1Yuu5$HckovDEjF0&-I4D70=&C3h~SU}S{!0S zi?FxS9m+y`k-01{F(RqFN002!uBN3RF|;19y*5~`iqSEq;dLUZ^_sM@SS+C-fBwd? zO5Z*7w-p)00HP1>7B)NcYHo>0o=Nzit9O1TF+kl1P@OFM2W}J|vk_G`{volB#B#V# zYFm`gG_XohCBGk!u&t4&6I~DbupcotzAO>B>KQjEe&QmL6r?O3n?l)`GrsyrkI$GU zY4X8()&_pZ1+{Hn_al_)jJG)=O%i+Ux$5^0PMWedt|XFFjO6F5quQIVOd1nOj=8ud z6}PYV3-;9y=VP_X_EH_91NYC?-2J$(dNbXUtjDC`zhPq4=1nKWVp)ejtsF#%CO0J9 za0BNkD*VPDuVSwEP1BBLdf|wqoM_EZ=^t&cz5Hs|&^;R5>p>mH0IIQ0^1I`oJ}V}M zRPGaiA^9q3r>kD5ZZ(AE_k8EQvTl=WzcXCI(_KL&A0G!47oBivMbaL(KzPhK3DE?v5W- z!4A9u-t?kii2mS=1bdn`x7BPliM2`zCjuIAR+VKc(4ihO%qk9{x@1KTdY#WB>=n}U zn;9CyrW^N1s~*Lut>GnydPPq8IJ+}Hesy5$u>;9kJx$N83H$M}%c2yG8gF#m>Uoav zw{SrX(-#lQ8oy))8}cK_7VS-61{|YD8r^V({_EPc2j815JR7lG7q zedhFM>7pa9o{%NIX`upjUhU1-OyQ``wIW)dz0f3Vq11e&7HM$CkF|7UTjQNz5w<(q zq%|HX#i6R`dDK#h-$-W!=Sam8=0ho(CLck=1xo!Bn#mtMXhZ4WeGQMPyXoCF;k0VZ z3nw!c5`l!1)|kg1$Y}Y{EspMIos^AZR1?c~JUY{Let2kOE@flM2>y?Yt1yy|OER7f zA<>y-X594TV4~f0X|#y+Px;ByD?$Tt{4`=&V{{wOSD9xsgM?rkf6pXM5kO4V+vdx} zG6UcJzYAXLHHFH^uRK6uUd+F9a5l&_fZ)ZM+orQ)6ap$1`;qz|a zR*$ggfTNImM{kD+kvdYQxuU$L~<$OLz()(!j@4Fw&p{*X{Q|z3fy*7M9kDL#r5# zQY5X1o0yogk^R4`U&vi*bPwqUoKr0MZMx5N*{{d78HDip-l_B${>Gql(mw;)izRa! zwXZ{HamBeAmgV(h)Ay)xs=!plOR5oTq%^-bV#&GkNBcOQkrvj`qL>Rhl7{QS4(#tc zcT~z$yl`Uy7!xNXTn=481~cWAKy95Vff;CuB#U4-ChJf*Lu76aACJL<55qKS4o5!$B&#) z)@(%Y)(EG~BvZdVz*iJL?(Usa!`<2VY`QEu5=(VsIt}`CZF^0d=Fa@enuwn{_*rI~ ze4sE#jNCfWtU&MH5Kfc0K$WUZtkCS`(Y2?t-s+|Y*UU0kL_PNK7a|4fBvC)rihrO~ zR!^>~4%wWb)vL?iK5#Jevf3PXrn>M~^R=ve9Ook`-nD11Q}nX-Go7iCat2 zdW+|xMW|6|3`LV}tgbfUbM=1dVte=nNT67%o6j0C9uDsSEG?I;ro%7GPU1Rgi zjUM*dioYQanFU2UacAc*yW7V_GZXYk`lAKZoCy8hwsT`T*>y#o15@=VbEC>}QlMfI z^X-fNXt)K7cToA?|7834-<0mJg`hznr`BneosS*!4<5vyuvd!(?~d25 z{LoY=;9{6#SnmZyHY9wC1HTIFKpu<5d0hybS z%;`AUYmREs>wwkVOp;02#+>1WZKOc4>KViSbXpc^b5Tg-ibC(OJ4q8WLS}QGP>mT)Sq+K`QmX8BAw7dKIpkbaqL| zabuRfxXFn8;u@cBh-;Q?YWVKve&y7Bw(k{bDzeUplRWBmOEQ9SlXpV2NunWTe)?Ar z!!t<+>5GVGtxB$+({Ge>?^=qMt7m7mRr;B9XI?F>U(Y$fYPT+Kd9kJ<2+<5&{A!{% z^e5Z5wrOViVXbf>Ikb@7t34_n@J1FRT(i%lCwVB z`&F0pF1k~#RY1D!!eaEh=sb#haXpr*o*Q zq+1qE^Hfu4e;LU;vgg}0HztgV31kMVeK&G(>jU%RKZe>od*aYtS5#NKzJM^jJ?K?q zMpe*7m&)_4Do4efduRMzxO1m^YFEs+iKl02k8uneSE=y2XGxd$x2%F>z7k*>GZ&g4 zp(p1<-eU9oS0HoF8rnS zLPP@pUhV0>yuE%E^`2kEj886tp8Cz3z0=#0`xvNu9*~T3qsZh;PHLU?o7_|9h*mg9 zVovJg5M!npXmm8oyjyirq~WW7otNQa`Z18<6d6sy36F0R4V}-{&mES=g8&ahAwpVk z@v@tRGB6qHt120d;mN?1v@>tzpxuFOHfvyBa!()cM{1knu7UYzYp!tKvOx#hwthlr z9~6<~_N(nnJ~bUe_)-8jHjcL}uN)I57X zHF^2PO5+W6=Rgv*@1lx8>4~)`DLCpZjzO8U56^E{~Cy8Ey2pExZWnVH&uyJ48jDqqY4bKJJ~ z{WH@~IU5tQyxumQ$GEZBiMwqlOl87y{8{zu$W!ByNx#j%`vQ( z5^UF1t}A2?sr3D|++LRwrFWN{{>MVahyK}Zc)!{pvasQ+1qSXdl`_45I-Uw-*+s!k z_80vl+4Vmp6~Iqch`<%vhi%gY>hI~4I!@mBPh!iK|FMaNWi$0!S=m26dq)cR-q3uZ zUbt*0vl}0B``BEQob3sO||Hha#mc7WNkyF@vpn4 zXiBuoVx?XBa-EsC7Npu_K|oYJHhZhtjZ7$6cmGV}yjv?!PyOk`86N&9bo2TPlyF5-dw3t! z87}@+$1)7WE_W+uFKrbpyB|TUxG!2Jtw;FFp<*ZV2;9{W83(gz1crgPHmC?pl*Pw5 z)feFB`(ubdOn_wr*Aw`!;oO0hLOZ~Bczo5}9?~&1?7tkY?MFVe`e9FuyC)<;gE%kh zX>Lvs2Ob!%LzIQQ9noK!7{de532Py{zxIjc3GH7oc;WI5B~CuI5+39p6@WC6cv;w$ z2|Y)XnMsG7E!c=Q;TtTO%XM(QS;5m&oa!8mUEp(fb|||XCbn`g!fb}pSBDIAv3*&QvITb|RQkt`Wkq@B+>Hl7#!s^7 zPIv%NgqrwcBy_u7nU@2bXCPlNJ5Yqy{sWt?uopJZK)O=FPlpR(NV{yf1<`=iAmN`! zMR%Tqin3uYmx3W;6cX3gUme%-Z}c^sK;~-3r$2_tC%1SS0q0RT%?_8)dHH8R{mQdt zMV5Mxhy9hI;Z(OEol4MN9P3J}^M0uVE~=pq#$CNEHsfg+qafNFYR!fGCy<*z#e{o|BN7$%CQ_sjdFC zQ2*swtI6gBYL5UoI1zaaITwY%+6vf(5zd}N5cv8*(Q5^*{;3c+kiME6MxdGqfCCbd zV`1~_&SC7_g|^ti3YG#yTk9*0+4N!S4Tz=@VInk}$ZzjVz1;{ui+|apZY4?qFC);^ z=3H4QPMMS{NCVCCgU;>ogr-Xmu1J~N@(H5N@cIkFplsSikf8dRAwX^|fZ<9m*Z2V{Hm#t+!$Wp~7BX`eYs$JMRX#PAY zS_gJrGQUraQ6RLbwv<{{TTI>gcc>}Od5@R_pF6jE57w}3%*eUtBJaQ}L5#Sxo?ZDM z(uZxyCX&5;MeNGGXQf?(q2nA(#cZX23fS7BdIulB#WLX3>HNY69!cYB1p+Q=TlHXe zo`R|!{~f`OXU{N{7M%Mos@{Hh1qPGCGw2WWA)4PkbDwGzY8on86TGqTsz>2IM#za@ z(|gbq7zvFl2a#-?q3;a8181tno;DPgt1jaM1L3ObI!B$W7;-2c=(y!#`*FEI=}uWH zas&-yA7FVssbu)bxH#s@2x^x9@l3SDLR@u7?rRhQku<4}LtrWNFo4CD1ZW6iLi?ZZ6<@7c|7pt9-Rg>kf?Wa?nm{qOj+s!7B|e}O#; zQ&iIRK7WwDoPT}Lp%Tl>`CZsnb9Ee-Eua?=cps5o!S`0#HH3e!_D9ro?E>--<>Q)U zYJS=sHOy36>`EneDiq!Gj9RESMdmR9*KF@C7Vv2N!DXL^SaniRj8qMMzIVK?JI2W> z?^n369o@KGA2A%+t~f2pcIvZL4#0diVVyOAx6|b3N9yL@Q1)uiFddD z{Rm8k7BRjw=4h6Y<6EqB4aPl)`=wLr@X`C@cus5|H+mO>$r{k~0SZ$N5H_OZ zAFR8Gfb{>}DWHUOO9A2k4-A*c|D!fm;k>q*W#;UIDVV#&ytnza_w66olO5raVV^B5 zEC$m4KpT}t+&?WDrNK-Vn8%LpjrKlyY^^$oI^dDVe_Rbo&2oft2y#iTlzufgeU00; z$+|ui58VPHzL!NFr(!zN;E5l5GJOq(P<{o&zB{hH;Xx1uM_+k78~RiCYrwIC=bWFp z{_fd-w2z0vf{Rh7LX9yqS@N5p@+rIlNLE3-W&s{mfH#gpJW2=!C-V?%loyA%!Z^eL zxtJIz&5ieu`c4az&F|B)VOf!c@uOwiOPBm*(7@&JXAaC8b*;Ihb6$-d%T}?4Wb$Cz!|* zOn^^s%tI;1CU& zDcmgqV^UG_1pa}+6@%#EXbk)LJAzP&2pjlZ#M+{Wj;E7G8N|O zF~vybojOr7_jm1A`G-^vk>uCF| zh)L!9=0!C%5U=vy(c(9LlYpitdpo&$Tw9!zJC0vCwep9H*3E7bPBn%Um?e`;Rnfnh zrpozXc9hF{dlemQ7}$MYgtkOG|F>@UTPPk69f+~4LnHrCujNQRh)*{YDu11bYN2rJ z%w%A!mF86m{PF=imn2*3jR0_s87Ui1x-4w|Xv|zrnruCaN+aJ&lyqK>c1mr30d`i~ zIIl*nVD0R_D{UIv2#;Ua))gx$JS99WG}?90*F~aGNQ;D;qSO_9%Y6sGS);u-WaU;+ z-)*~Ng^WpaA~G1vZ#$w_otT|Elw5lpsoruS*$053kfxO24~Y-dCZ+R5;39ir-8uv!9{|{~XbepQ_w5w}x?(w07jdE*I_H472d62J0!&3fNv zkA9XWXHOciJAQ1orZo1dp-_m1~IH0beJ{PX!i50<_Bv-H8w$S*SnbWbeQ&+BZc9Czz{2Yp9;m+Q($+~Noo>A5R=q#nRbgH-e z=gU%}$w3@hp1g-_#x61$4Wh(Yi=Pm-UODJ&do+4>x(_Wh?!4d>X2vn9xiq`>a}sHB zC;g{!`mFYeEv)#Tv8+Q!dV>A6N^&fAgD8v809>3i{c__LT3d1Mc<*)$U|Yu8UnY{e z9*RY&PFN#)+kfjJ$<2FX;MMBrx0%Dn+#4;jNIR*Q9*t+htJN6#?9Q8w#Y&0}w(Eck zZDvYvF}U5=?r5vpf)56Kke~O=PlYp-Ez$TFf)DOox&!Q@qr20mPCz(GU#CO}Ck=pF zMWqCZ{&Ti-9G}=TEoV2~;fMRWYques^zixs?W&8xhZPd;fIHeF3{8^zvrYD;t!j8k z|C-c(7p8nIk>nd8GogJ3dYqNOqw)7`)ua=T{5Sn7YQB}d4rF*?s-E}bT#_fYvXe8u`#I6irXXFGTbQ54;f}R29jS=pQFFQM2OQ+pOD=a|z@;S|=N; z2fM!zSU0EPeU$snU2ietXGG__#5biuIuzaWQq&{ElWcokNxr#bjzI&Iu((rA)|jMzx(38$5F4sD~a9Q9z|8`qw69~HNo9*L8W zI{8d(51paC1frybhd5P}0MuwrcqRMvaPGnzync6b2)hcB@#ukMmQ>Ai&hE}4mK~pF zaE7IO3UEfBQW>&{eDCZJ2;904eNmiy++M{8*i7MCh=bOf>5Mub(I(L-BCNG^-xYp0 zBN{5nA*b>aVTKi@U`|Vuq~10IKo9@v;uy}E{xQgeLts5RVOdfY4xal6odC++Xp0t! zRkR7u4X4PsR?H|PKaS#taB0WKhae+rw%YBbYv2jNdp8D!=4k=zPdszOy@(k(T#JCpi1N8oTQ^TfV!fm&=J9vD)HAH_4eYRVx{D63IOwB zev0a(i+8V6Znda6nysizt^nHM`W&IqsCO-fq{-42+$NPl2PFq#fV9!z>cz%n5F=jX zpo>uWl$QVmh#kd*l0((#jC*m*&9#`xrpW{VvOzz9^-B$sF3SS#|1og?*zqj@p=1?} zVLAl2?H-pyKnTVQUfY_U6ay8s5Ntno5kCjvTOXpi6Kr4JLg!TgbcAA+K3t$2(nmT{E}Qv{;?Nw^$EfP7|w_FDMDLK?iFd!_f>QpN)GEyG1P zGFbqvTMw#55xv$AHTZ`hIWRNjbsyUzJA;(EISB*4=U!;A2i`cgqX3zvWO-D%Q;?Te`Vek-L1TRYYJ0zjKI-`Pit)cj_C<)+Qv+szR317reVk@MR3zvzDbim zG8vDTR+`H+fW3+XDWz;A_v0pX0v;-vj~_X^_g4Y~;-eA1W2oU|0)~84DJ$48R=g%M z)R5k7Y6ixz{vcj{AO%kb3U3}WZn>*SpIA1KLUcTH`9Tw(O4{RfrtOoYB1qgQ9VHuz z4V5&qg_pRe4HlnO{FHDf6w0`31`T)#(ozrVlX< z*oM5>VE&g6r@L1s?YeI&LLi=}#nHxaKIcyp%UrI~zqj(0ShWj#z}&gh3?n=eE2@P!&!g+7lG-iAJVa}F8}KXT7m~%0s4Hk Ziiev5|9e#>24A27lH(4CDtq$T{{!vR^_Tzv literal 9664 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 b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 080d6c7..a65bb1a 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "CMUQ GenEd", + "name": "CMU-Q General Education Course Planner", "icons": [ { "src": "favicon.ico", @@ -20,6 +20,6 @@ ], "start_url": ".", "display": "standalone", - "theme_color": "#000000", + "theme_color": "#600000", "background_color": "#ffffff" } From 81d1c3ee990cda58956c3634999e12684021ae4b Mon Sep 17 00:00:00 2001 From: jullia02 Date: Sat, 19 Apr 2025 11:03:55 +0300 Subject: [PATCH 02/22] changed readme file --- frontend/README.md | 147 +++++++++++++++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 38 deletions(-) diff --git a/frontend/README.md b/frontend/README.md index 58beeac..f681116 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,70 +1,141 @@ -# Getting Started with Create React App +# GenEd Frontend -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +This directory contains the React.js frontend application for the CMU-Q General Education course planning platform. It provides a searchable, filterable UI to explore and plan general education and core requirements across different majors. + +--- -## Available Scripts +## Getting Started + +### Prerequisites -In the project directory, you can run: +* Node.js (v16+ recommended) +* npm (comes with Node.js) + +### Setup + +1. **Navigate to the frontend directory:** + ```bash + cd path/to/GenEd-CMUQ/frontend + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` -### `npm start` +3. **Run the development server:** + ```bash + npm start + ``` -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. +The app will be accessible at `http://localhost:3000`. -The page will reload when you make changes.\ -You may also see any lint errors in the console. +> ⚠️ Make sure the backend is running and available at the correct API base URL. You can configure this in `.env`: +> ```env +> REACT_APP_API_BASE_URL=http://127.0.0.1:8000 +> ``` -### `npm test` +--- -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +## Features -### `npm run build` +- πŸ” Search for courses by department and course number. +- βœ… Filter by campus offering, prerequisites, GenEd/Core types, and requirement fulfillment. +- πŸ“š View courses fulfilling specific requirements. +- πŸ“Š Paginated results for easy navigation. +- 🧩 Modular components with popup detail views. -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +--- -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! +## Components Overview -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. +| Component | Purpose | +|----------|---------| +| `CourseTablePage.js` | Top-level controller for state and layout. Combines filters, search bar, and table. | +| `SearchBar.js` | Handles department selection, search input, and checkbox filters. | +| `CourseTable.js` | Renders the course data in a table format with dynamic requirement columns. | +| `MultiSelectDropdown.js` | Generic dropdown for selecting multiple filters (e.g. requirements, semesters). | +| `MultiSelectDepartment.js` | Specialized dropdown for department filtering. | +| `SelectedFilters.js` | Shows active filters with remove buttons. | +| `PopUp.js` | Modal that shows detailed course or requirement information. | -### `npm run eject` +--- -**Note: this is a one-way operation. Once you `eject`, you can't go back!** +## File Structure -If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. +``` +frontend/ +β”‚ +β”œβ”€β”€ public/ # Static files +β”‚ β”œβ”€β”€ index.html +β”‚ β”œβ”€β”€ manifest.json # PWA settings +β”‚ └── icons # App icons +β”‚ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ components/ # React components +β”‚ β”‚ β”œβ”€β”€ CourseTablePage.js +β”‚ β”‚ β”œβ”€β”€ CourseTable.js +β”‚ β”‚ β”œβ”€β”€ SearchBar.js +β”‚ β”‚ β”œβ”€β”€ PopUp.js +β”‚ β”‚ β”œβ”€β”€ MultiSelectDropdown.js +β”‚ β”‚ β”œβ”€β”€ MultiSelectDepartment.js +β”‚ β”‚ └── SelectedFilters.js +β”‚ β”œβ”€β”€ styles.css # App styling +β”‚ └── index.js # Entry point +β”‚ +β”œβ”€β”€ .env # API base URL config +β”œβ”€β”€ package.json # Project metadata & dependencies +└── README.md # This file +``` -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. +--- -You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. +## Running Tests -## Learn More +The frontend uses **Jest** and **React Testing Library** for testing. -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +### Running All Tests +```bash +npm test +``` -To learn React, check out the [React documentation](https://reactjs.org/). +### Running a Specific Test File +```bash +npm test CourseTable.test.js +``` -### Code Splitting +### Example Test Files +Tests are located in the `frontend/tests/` directory and include: -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +``` +tests/ +β”œβ”€β”€ CourseTable.test.js +β”œβ”€β”€ SearchBar.test.js +β”œβ”€β”€ CourseTablePage.test.js +β”œβ”€β”€ MultiSelectDropdown.test.js +└── ... +``` -### Analyzing the Bundle Size +Tests cover component rendering, user interactions, and prop handling. Each test simulates real user behavior using `@testing-library/react`. -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +> Make sure all required props are mocked correctly in your tests to avoid runtime errors. -### Making a Progressive Web App +--- -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +## Development Notes -### Advanced Configuration +- Data is fetched dynamically via REST API endpoints from the FastAPI backend. +- API errors are handled in `useEffect` hooks with simple `console.error()` logsβ€”consider improving with user notifications. +- The UI is responsive and works across modern browsers. -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) +--- -### Deployment +## Progressive Web App (PWA) -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) +The frontend includes a `manifest.json` file to support installation as a PWA. Icons and colors can be customized in `public/manifest.json`. -### `npm run build` fails to minify +--- -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +## Authors + +This project was developed by CMU-Q Students, Boushra, Jullia, and Yousuf, for the **67-373 Information Systems Consulting Project**. From f285106401913686b9043d81c30fd49340a67b78 Mon Sep 17 00:00:00 2001 From: jullia02 Date: Sat, 19 Apr 2025 11:14:38 +0300 Subject: [PATCH 03/22] removed unused lines --- frontend/src/App.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/App.js b/frontend/src/App.js index 11ba171..169afcc 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,9 +1,6 @@ -// import CourseTableMock from "./components/courseTableMock/courseTableMock.js"; -// import CourseTableMock from "./components/CourseTablePage"; import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import MainTabs from "./components/MainTabs"; -// import CourseTablePage from './components/CourseTablePage'; import DataUpload from './components/DataUpload'; import AnalyticsPage from './components/Analytics'; import "./styles.css"; From 76fbcccd9fdb6c5a6121bc270772a716d6c06831 Mon Sep 17 00:00:00 2001 From: jullia <152446623+jullia02@users.noreply.github.com> Date: Sat, 19 Apr 2025 11:48:01 +0300 Subject: [PATCH 04/22] Update README.md --- frontend/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/README.md b/frontend/README.md index f681116..8afa274 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -92,7 +92,7 @@ frontend/ ## Running Tests -The frontend uses **Jest** and **React Testing Library** for testing. +The frontend uses **Jest** and **React Testing Library** for testing. Make sure you are in the frontend folder before running tests. ### Running All Tests ```bash From 6226666befc36dc200da7fe0e3398cb7f0a40c17 Mon Sep 17 00:00:00 2001 From: jullia02 Date: Sat, 19 Apr 2025 13:02:32 +0300 Subject: [PATCH 05/22] moved tests to src folder and added tests --- .../src/{App.test.js => App.test.skip.js} | 0 frontend/src/tests/Analytics.test.js | 38 +++++++ frontend/src/tests/CategoryCoverage.test.js | 59 ++++++++++ frontend/{ => src}/tests/CourseTable.test.js | 2 +- .../{ => src}/tests/CourseTablePage.test.js | 2 +- frontend/src/tests/DataUpload.test.js | 43 ++++++++ .../src/tests/EnrollmentAnalytics.test.js | 50 +++++++++ frontend/{ => src}/tests/MainTabs.test.js | 9 +- .../tests/MultiSelectDropdown.test.js | 24 +---- .../{ => src}/tests/PlanCourseTab.test.js | 101 ++++++++++++------ frontend/{ => src}/tests/PopUp.test.js | 2 +- frontend/{ => src}/tests/SearchBar.test.js | 11 +- .../{ => src}/tests/SelectedFilters.test.js | 29 +---- .../tests/SingleSelectDropdown.test.js | 2 +- frontend/tests/EnrollmentAnalytics.test.js | 71 ------------ 15 files changed, 270 insertions(+), 173 deletions(-) rename frontend/src/{App.test.js => App.test.skip.js} (100%) create mode 100644 frontend/src/tests/Analytics.test.js create mode 100644 frontend/src/tests/CategoryCoverage.test.js rename frontend/{ => src}/tests/CourseTable.test.js (98%) rename frontend/{ => src}/tests/CourseTablePage.test.js (98%) create mode 100644 frontend/src/tests/DataUpload.test.js create mode 100644 frontend/src/tests/EnrollmentAnalytics.test.js rename frontend/{ => src}/tests/MainTabs.test.js (75%) rename frontend/{ => src}/tests/MultiSelectDropdown.test.js (79%) rename frontend/{ => src}/tests/PlanCourseTab.test.js (60%) rename frontend/{ => src}/tests/PopUp.test.js (98%) rename frontend/{ => src}/tests/SearchBar.test.js (87%) rename frontend/{ => src}/tests/SelectedFilters.test.js (54%) rename frontend/{ => src}/tests/SingleSelectDropdown.test.js (96%) delete mode 100644 frontend/tests/EnrollmentAnalytics.test.js diff --git a/frontend/src/App.test.js b/frontend/src/App.test.skip.js similarity index 100% rename from frontend/src/App.test.js rename to frontend/src/App.test.skip.js diff --git a/frontend/src/tests/Analytics.test.js b/frontend/src/tests/Analytics.test.js new file mode 100644 index 0000000..6b9748f --- /dev/null +++ b/frontend/src/tests/Analytics.test.js @@ -0,0 +1,38 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import AnalyticsPage from '../components/Analytics'; +import '@testing-library/jest-dom'; + +jest.mock('../components/CategoryCoverage', () => () => ( +
Category Coverage Component
+)); + +jest.mock('../components/EnrollmentAnalytics', () => () => ( +
Enrollment Analytics Component
+)); + +describe('AnalyticsPage', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('renders the page title and subtitle', () => { + render(); + expect(screen.getByText('Analyze Courses')).toBeInTheDocument(); + expect(screen.getByText(/Visualize requirement coverage/i)).toBeInTheDocument(); + }); + + it('saves selectedMajor to localStorage', () => { + render(); + expect(localStorage.getItem('analyticsMajor')).toBe('bio'); + }); + + it('renders CategoryCoverage and EnrollmentAnalytics components', () => { + render(); + expect(screen.getByTestId('category-coverage')).toBeInTheDocument(); + expect(screen.getByTestId('enrollment-analytics')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/tests/CategoryCoverage.test.js b/frontend/src/tests/CategoryCoverage.test.js new file mode 100644 index 0000000..f67e07e --- /dev/null +++ b/frontend/src/tests/CategoryCoverage.test.js @@ -0,0 +1,59 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { render, screen} from '@testing-library/react'; +import CategoryCoverage from '../components/CategoryCoverage'; +import '@testing-library/jest-dom'; + +// Mock Plotly +jest.mock('react-plotly.js', () => () =>
); + +const mockMajors = { + is: 'Information Systems', + ba: 'Business Administration', + cs: 'Computer Science', + bio: 'Biological Sciences' +}; + +describe('CategoryCoverage', () => { + beforeEach(() => { + global.fetch = jest.fn((url) => { + if (url.includes('/courses/semesters')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ semesters: ['F23', 'S23'] }), + }); + } + if (url.includes('/analytics/course-coverage')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ + coverage: [ + { requirement: 'GenEd---Science---Lab', num_courses: 2 }, + { requirement: 'GenEd---Math---Calculus', num_courses: 3 } + ] + }), + }); + } + return Promise.reject(new Error('Unknown API')); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('renders Plotly graph if data is available', async () => { + render( + {}} + majors={mockMajors} + /> + ); + + const plot = await screen.findByTestId('mock-plot'); + expect(plot).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/CourseTable.test.js b/frontend/src/tests/CourseTable.test.js similarity index 98% rename from frontend/tests/CourseTable.test.js rename to frontend/src/tests/CourseTable.test.js index 2c5e78d..fbdb2ef 100644 --- a/frontend/tests/CourseTable.test.js +++ b/frontend/src/tests/CourseTable.test.js @@ -4,7 +4,7 @@ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import CourseTable from '../src/components/CourseTable.js'; +import CourseTable from '../components/CourseTable.js'; const mockCourses = [ { diff --git a/frontend/tests/CourseTablePage.test.js b/frontend/src/tests/CourseTablePage.test.js similarity index 98% rename from frontend/tests/CourseTablePage.test.js rename to frontend/src/tests/CourseTablePage.test.js index 7bcdee1..c0962a4 100644 --- a/frontend/tests/CourseTablePage.test.js +++ b/frontend/src/tests/CourseTablePage.test.js @@ -3,7 +3,7 @@ */ import React from 'react'; import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react'; -import CourseTablePage from '../src/components/CourseTablePage'; +import CourseTablePage from '../components/CourseTablePage'; import '@testing-library/jest-dom'; // Mock localStorage diff --git a/frontend/src/tests/DataUpload.test.js b/frontend/src/tests/DataUpload.test.js new file mode 100644 index 0000000..5fa049c --- /dev/null +++ b/frontend/src/tests/DataUpload.test.js @@ -0,0 +1,43 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import DataUpload from '../components/DataUpload'; +import '@testing-library/jest-dom'; + +// Mock global fetch +beforeEach(() => { + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ message: 'Success' }), + }) + ); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe('DataUpload Component', () => { + test('renders key elements and upload button disabled initially', () => { + render(); + + expect(screen.getByText(/Database Data Upload/i)).toBeInTheDocument(); + expect(screen.getByText(/Select Course ZIP Files/i)).toBeInTheDocument(); + expect(screen.getByText(/Upload Data/i)).toBeDisabled(); + }); + + test('shows error when uploading invalid department file', async () => { + render(); + const file = new File(['bad content'], 'departments.txt', { type: 'text/plain' }); + + const input = screen.getByLabelText(/Select Department CSV File/i); + fireEvent.change(input, { target: { files: [file] } }); + + await waitFor(() => { + expect(screen.getByText(/Department file must be a CSV file/i)).toBeInTheDocument(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/tests/EnrollmentAnalytics.test.js b/frontend/src/tests/EnrollmentAnalytics.test.js new file mode 100644 index 0000000..576ffc6 --- /dev/null +++ b/frontend/src/tests/EnrollmentAnalytics.test.js @@ -0,0 +1,50 @@ +/** + * @jest-environment jsdom + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import EnrollmentAnalytics from '../components/Analytics'; +import '@testing-library/jest-dom'; + +// Patch for process.env +const API_BASE_URL = 'http://localhost'; + +jest.mock('react-plotly.js', () => (props) => ( +
+ Plotly Graph: {props.layout?.title} +
+)); + +beforeAll(() => { + global.fetch = jest.fn((url) => { + const expectedUrl = `${API_BASE_URL}/analytics/enrollment-data?course_code=76-101`; + + if (url === expectedUrl) { + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + enrollment_data: [ + { semester: 'F22', enrollment_count: 25, class_: 1 }, + { semester: 'S23', enrollment_count: 30, class_: 2 }, + ], + }), + }); + } + + return Promise.reject(new Error(`Unexpected URL: ${url}`)); + }); +}); + +afterAll(() => { + global.fetch.mockRestore(); +}); + +describe('EnrollmentAnalytics', () => { + test('renders input fields and buttons', () => { + render(); + expect(screen.getAllByPlaceholderText(/enter course code/i)).toHaveLength(2); + expect(screen.getByText(/add course/i)).toBeInTheDocument(); + expect(screen.getByText(/load course/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/tests/MainTabs.test.js b/frontend/src/tests/MainTabs.test.js similarity index 75% rename from frontend/tests/MainTabs.test.js rename to frontend/src/tests/MainTabs.test.js index 5c13a4f..381b0f1 100644 --- a/frontend/tests/MainTabs.test.js +++ b/frontend/src/tests/MainTabs.test.js @@ -3,13 +3,12 @@ */ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import MainTabs from '../src/components/MainTabs'; +import MainTabs from '../components/MainTabs'; import '@testing-library/jest-dom'; -// Mock child components so we can isolate MainTabs behavior -jest.mock('../src/components/CourseTablePage', () => () =>
Course Table Page
); -jest.mock('../src/components/PlanCourseTab', () => () =>
Plan Course Tab
); -jest.mock('../src/components/Analytics', () => () =>
Analytics Page
); +jest.mock('../components/CourseTablePage', () => () =>
Course Table Page
); +jest.mock('../components/PlanCourseTab', () => () =>
Plan Course Tab
); +jest.mock('../components/Analytics', () => () =>
Analytics Page
); describe('MainTabs', () => { beforeEach(() => { diff --git a/frontend/tests/MultiSelectDropdown.test.js b/frontend/src/tests/MultiSelectDropdown.test.js similarity index 79% rename from frontend/tests/MultiSelectDropdown.test.js rename to frontend/src/tests/MultiSelectDropdown.test.js index 6bc6428..65da196 100644 --- a/frontend/tests/MultiSelectDropdown.test.js +++ b/frontend/src/tests/MultiSelectDropdown.test.js @@ -4,7 +4,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import MultiSelectDropdown from '../src/components/MultiSelectDropdown.js'; +import MultiSelectDropdown from '../components/MultiSelectDropdown.js'; describe('MultiSelectDropdown', () => { const mockHandleChange = jest.fn(); @@ -34,28 +34,6 @@ describe('MultiSelectDropdown', () => { expect(screen.getByText(/clear all/i)).toBeInTheDocument(); }); - test('calls filterChange when selecting option', () => { - render( - - ); - - fireEvent.click(screen.getByText(/select/i)); // open dropdown - - const groupToggle = screen.getByText(/core/i); - fireEvent.click(groupToggle); - - const checkbox = screen.getByRole('checkbox', { name: /humanities/i }); - fireEvent.click(checkbox); - - expect(mockHandleChange).toHaveBeenCalledWith("BA", ["core---humanities"]); - }); - test('calls clearFilters when "CLEAR ALL" is clicked', () => { render( { const store = {}; jest.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => store[key] || null); @@ -15,7 +14,6 @@ beforeEach(() => { }); }); -// Mock fetch beforeAll(() => { global.fetch = jest.fn((url) => { if (url.includes('/requirements')) { @@ -98,59 +96,98 @@ describe('PlanCourseTab', () => { render(); expect(screen.getByText(/Plan Courses/i)).toBeInTheDocument(); expect( - screen.getByText(/Add courses from the main table or search to build/i) + screen.getByText(/Add courses from the main table or search/i) ).toBeInTheDocument(); }); test('searches and adds a course to the plan', async () => { render(); - const input = screen.getByPlaceholderText(/Search by course code/i); + const input = screen.getByPlaceholderText(/search by course code/i); + const searchBtn = screen.getByRole('button', { name: /πŸ”/i }); + fireEvent.change(input, { target: { value: '76-101' } }); - fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); - - await screen.findByText(/76-101/i); - fireEvent.click(screen.getByText('+ Add')); - - await screen.findByText(/Course added! πŸŽ‰/i); + fireEvent.click(searchBtn); + + await waitFor(() => { + expect( + screen.getByText((content, element) => + element?.textContent?.includes('76-101') + ) + ).toBeInTheDocument(); + }); + + const addBtn = screen.getByRole('button', { name: /\+ Add/i }); + fireEvent.click(addBtn); + + await waitFor(() => { + expect(screen.getByText(/Course added! πŸŽ‰/i)).toBeInTheDocument(); + }); }); - + test('prevents duplicate addition', async () => { render(); - const input = screen.getByPlaceholderText(/Search by course code/i); + const input = screen.getByPlaceholderText(/search by course code/i); + const searchBtn = screen.getByRole('button', { name: /πŸ”/i }); + fireEvent.change(input, { target: { value: '76-101' } }); - fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + fireEvent.click(searchBtn); - await screen.findByText('+ Add'); - fireEvent.click(screen.getByText('+ Add')); + await waitFor(() => { + expect( + screen.getByText((content, element) => + element?.textContent?.includes('76-101') + ) + ).toBeInTheDocument(); + }); - // Wait for the toast AND the button to change - const buttons = await screen.findAllByText(/Added/i); - const addedButton = buttons.find((btn) => btn.tagName === 'BUTTON'); - expect(addedButton).toBeInTheDocument(); + const addBtn = screen.getByRole('button', { name: /\+ Add/i }); + fireEvent.click(addBtn); - fireEvent.click(addedButton); - expect(await screen.findByText(/Course already added! πŸ˜…/i)).toBeInTheDocument(); - }); + await waitFor(() => { + expect(screen.getByText(/Course added!/i)).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: /Added/i })); + + await waitFor(() => { + expect(screen.getByText(/Course already added!/i)).toBeInTheDocument(); + }); + }); test('clears all courses when confirmed', async () => { window.confirm = jest.fn(() => true); + render(); + const input = screen.getByPlaceholderText(/search by course code/i); + const searchBtn = screen.getByRole('button', { name: /πŸ”/i }); - const input = screen.getByPlaceholderText(/Search by course code/i); fireEvent.change(input, { target: { value: '76-101' } }); - fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + fireEvent.click(searchBtn); - await screen.findByText('+ Add'); - fireEvent.click(screen.getByText('+ Add')); + await waitFor(() => { + expect( + screen.getByText((content, element) => + element?.textContent?.includes('76-101') + ) + ).toBeInTheDocument(); + }); + + const addBtn = screen.getByRole('button', { name: /\+ Add/i }); + fireEvent.click(addBtn); - const table = screen.getByRole('table'); - const courseCell = await within(table).findByText('76-101'); - expect(courseCell).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(/Course added!/i)).toBeInTheDocument(); + }); + + const table = await screen.findByRole('table'); + expect(table).toBeInTheDocument(); fireEvent.click(screen.getByText(/Clear All/i)); await waitFor(() => { - expect(within(table).queryByText('76-101')).not.toBeInTheDocument(); + expect(screen.getByText(/No planned courses yet/i)).toBeInTheDocument(); }); }); + + }); \ No newline at end of file diff --git a/frontend/tests/PopUp.test.js b/frontend/src/tests/PopUp.test.js similarity index 98% rename from frontend/tests/PopUp.test.js rename to frontend/src/tests/PopUp.test.js index 1b86798..e7a73d8 100644 --- a/frontend/tests/PopUp.test.js +++ b/frontend/src/tests/PopUp.test.js @@ -4,7 +4,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import Popup from '../src/components/PopUp.js'; +import Popup from '../components/PopUp.js'; const mockCourse = { course_code: '76-101', diff --git a/frontend/tests/SearchBar.test.js b/frontend/src/tests/SearchBar.test.js similarity index 87% rename from frontend/tests/SearchBar.test.js rename to frontend/src/tests/SearchBar.test.js index ff1060c..ca732a0 100644 --- a/frontend/tests/SearchBar.test.js +++ b/frontend/src/tests/SearchBar.test.js @@ -4,7 +4,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import SearchBar from '../src/components/SearchBar.js'; +import SearchBar from '../components/SearchBar.js'; beforeAll(() => { global.fetch = jest.fn(() => @@ -46,15 +46,6 @@ describe('SearchBar', () => { return props; }; - test('renders and updates course search input', () => { - const { setSearchQuery } = setup(); - - const input = screen.getByPlaceholderText(/specific course number/i); - fireEvent.change(input, { target: { value: '76-101' } }); - - expect(setSearchQuery).toHaveBeenCalled(); - }); - test('renders selected department tag and clears on X click', () => { const { setSelectedDepartments } = setup({ selectedDepartments: ['76'], diff --git a/frontend/tests/SelectedFilters.test.js b/frontend/src/tests/SelectedFilters.test.js similarity index 54% rename from frontend/tests/SelectedFilters.test.js rename to frontend/src/tests/SelectedFilters.test.js index d95e6a5..78a232f 100644 --- a/frontend/tests/SelectedFilters.test.js +++ b/frontend/src/tests/SelectedFilters.test.js @@ -4,7 +4,7 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; -import SelectedFilters from '../src/components/SelectedFilters.js'; +import SelectedFilters from '../components/SelectedFilters.js'; describe('SelectedFilters', () => { const mockHandleFilterChange = jest.fn(); @@ -33,33 +33,6 @@ describe('SelectedFilters', () => { expect(mockRemoveOfferedSemester).toHaveBeenCalledWith('F23'); }); - - test('renders requirement filters and calls handleFilterChange on X click', () => { - render( - - ); - - expect(screen.getByText('Information Security and Privacy β†’ Regulatory and Behavioral Core')).toBeInTheDocument(); - const matches = screen.getAllByText((content, element) => - element?.textContent.includes('Lab Requirement') - ); - expect(matches.length).toBeGreaterThan(0); - - - const closeButtons = screen.getAllByRole('button'); - fireEvent.click(closeButtons[0]); - - expect(mockHandleFilterChange).toHaveBeenCalledWith('IS', 'BS in Information Systems---Concentration---Information Security and Privacy---Regulatory and Behavioral Core'); - }); - test('returns null when there are no selected filters or semesters', () => { render( { const mockOnChange = jest.fn(); diff --git a/frontend/tests/EnrollmentAnalytics.test.js b/frontend/tests/EnrollmentAnalytics.test.js deleted file mode 100644 index ee17176..0000000 --- a/frontend/tests/EnrollmentAnalytics.test.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @jest-environment jsdom - */ -import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import EnrollmentAnalytics from '../src/components/Analytics'; -import '@testing-library/jest-dom'; - -// Mock Plotly -jest.mock('react-plotly.js', () => (props) => { - return ( -
- Plotly Graph: {props.layout?.title} -
- ); -}); - -// Mock fetch -beforeAll(() => { - global.fetch = jest.fn((url) => { - if (url.includes('76-101')) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - enrollment_data: [ - { semester: 'F22', enrollment_count: 25, class_: 1 }, - { semester: 'S23', enrollment_count: 30, class_: 2 }, - ], - }), - }); - } - return Promise.reject(new Error('Unknown endpoint: ' + url)); - }); -}); - -afterAll(() => { - global.fetch.mockRestore(); -}); - -describe('EnrollmentAnalytics', () => { - test('renders input fields and buttons', () => { - render(); - expect(screen.getAllByPlaceholderText(/enter course code/i)).toHaveLength(2); - expect(screen.getByText(/add course/i)).toBeInTheDocument(); - expect(screen.getByText(/load course/i)).toBeInTheDocument(); - }); - - test('fetches and displays aggregated enrollment plot', async () => { - render(); - const input = screen.getAllByPlaceholderText(/enter course code/i)[0]; - fireEvent.change(input, { target: { value: '76-101' } }); - fireEvent.click(screen.getByText(/add course/i)); - - await waitFor(() => { - const plots = screen.getAllByTestId('mock-plot'); - expect(plots.some(p => p.textContent.includes("Aggregated Enrollment Over Semesters"))).toBe(true); - }); - }); - - test('fetches and displays class-based enrollment plot', async () => { - render(); - const input = screen.getAllByPlaceholderText(/enter course code/i)[1]; - fireEvent.change(input, { target: { value: '76-101' } }); - fireEvent.click(screen.getByText(/load course/i)); - - await waitFor(() => { - const plots = screen.getAllByTestId('mock-plot'); - expect(plots.some(p => p.textContent.includes("Enrollment for 76-101"))).toBe(true); - }); - }); -}); From aa4d3b1ab1087ae9c196287aa54469f0edbde7db Mon Sep 17 00:00:00 2001 From: jullia02 Date: Sat, 19 Apr 2025 13:02:53 +0300 Subject: [PATCH 06/22] added read me for all components --- frontend/README.md | 4 ++ frontend/docs/AnalyticsPage.md | 67 +++++++++++++++++ frontend/docs/CategoryCoverage.md | 72 +++++++++++++++++++ frontend/docs/CourseTable.md | 93 ++++++++++++++++++++++++ frontend/docs/CourseTablePage.md | 87 ++++++++++++++++++++++ frontend/docs/DataUpload.md | 60 ++++++++++++++++ frontend/docs/EnrollmentAnalytics.md | 70 ++++++++++++++++++ frontend/docs/MainTabs.md | 69 ++++++++++++++++++ frontend/docs/MultiSelectDropdown.md | 78 ++++++++++++++++++++ frontend/docs/PlanCourseTab.md | 86 ++++++++++++++++++++++ frontend/docs/PopUp.md | 96 +++++++++++++++++++++++++ frontend/docs/SearchBar.md | 100 ++++++++++++++++++++++++++ frontend/docs/SelectedFilters.md | 81 +++++++++++++++++++++ frontend/docs/SingleSelectDropdown.md | 68 ++++++++++++++++++ 14 files changed, 1031 insertions(+) create mode 100644 frontend/docs/AnalyticsPage.md create mode 100644 frontend/docs/CategoryCoverage.md create mode 100644 frontend/docs/CourseTable.md create mode 100644 frontend/docs/CourseTablePage.md create mode 100644 frontend/docs/DataUpload.md create mode 100644 frontend/docs/EnrollmentAnalytics.md create mode 100644 frontend/docs/MainTabs.md create mode 100644 frontend/docs/MultiSelectDropdown.md create mode 100644 frontend/docs/PlanCourseTab.md create mode 100644 frontend/docs/PopUp.md create mode 100644 frontend/docs/SearchBar.md create mode 100644 frontend/docs/SelectedFilters.md create mode 100644 frontend/docs/SingleSelectDropdown.md diff --git a/frontend/README.md b/frontend/README.md index f681116..8402e1f 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -71,6 +71,8 @@ frontend/ β”‚ β”œβ”€β”€ manifest.json # PWA settings β”‚ └── icons # App icons β”‚ +β”œβ”€β”€ docs/ # Documentation on all components +β”‚ β”œβ”€β”€ src/ β”‚ β”œβ”€β”€ components/ # React components β”‚ β”‚ β”œβ”€β”€ CourseTablePage.js @@ -81,7 +83,9 @@ frontend/ β”‚ β”‚ β”œβ”€β”€ MultiSelectDepartment.js β”‚ β”‚ └── SelectedFilters.js β”‚ β”œβ”€β”€ styles.css # App styling +β”‚ β”œβ”€β”€ planTabStyles.css # App styling for planning tab β”‚ └── index.js # Entry point +β”‚ └── App.js β”‚ β”œβ”€β”€ .env # API base URL config β”œβ”€β”€ package.json # Project metadata & dependencies diff --git a/frontend/docs/AnalyticsPage.md b/frontend/docs/AnalyticsPage.md new file mode 100644 index 0000000..49fa3c6 --- /dev/null +++ b/frontend/docs/AnalyticsPage.md @@ -0,0 +1,67 @@ +# Component: `` + +## Purpose + +Serves as the main analytics dashboard in the GenEd platform. Displays visual insights on: +- **Requirement Category Coverage** for a selected major +- **Enrollment Trends** across semesters and student classes + +--- + +## Internal State + +| State Variable | Description | +|------------------|-------------| +| `selectedMajor` | Currently selected major code (e.g., `"cs"`, `"bio"`) β€” saved in `localStorage` | + +--- + +## Logic Overview + +- Renders two major analytics sections: + - ``: shows how many courses fulfill each requirement category + - ``: visualizes overall enrollment and enrollment by class year +- Uses localStorage to persist the selected major across page reloads + +--- + +## Example Usage + +```jsx + +``` + +Used in the **Analytics** tab of the GenEd app. This component provides an overview of course coverage and participation trends per major. + +--- + +## Related Components + +- `` β€” Major/semester-wise requirement category chart +- `` β€” Enrollment comparison charts + +--- + +## Related Tests + +- `AnalyticsPage.test.js` β€” covers: + - LocalStorage persistence of `selectedMajor` + - Rendering both child components + - UI behavior when switching majors + +--- + +## Styling + +| Class | Description | +|-------|-------------| +| `analytics-container` | Main page layout | +| `title`, `subtitle` | Page header styles | +| `
` | Section separator | + +--- + +## Notes + +- Majors are hardcoded in the component for display purposes, but can be dynamically sourced if backend supports it. +- The selected major is passed to both analytics views for synchronized filtering. diff --git a/frontend/docs/CategoryCoverage.md b/frontend/docs/CategoryCoverage.md new file mode 100644 index 0000000..1cf44b7 --- /dev/null +++ b/frontend/docs/CategoryCoverage.md @@ -0,0 +1,72 @@ +# Component: `` + +## Purpose + +Displays a bar chart showing how many courses fulfill each requirement category for a selected major and semester. Uses Plotly for charting and dynamically fetches data from the backend via `/analytics/course-coverage`. + +--- + +## Props + +| Prop Name | Type | Required | Description | +|-------------------|----------|----------|-------------| +| `selectedMajor` | String | βœ… | The major currently selected (e.g., "CS") | +| `setSelectedMajor`| Function | βœ… | Updates the selected major | +| `majors` | Object | βœ… | Dictionary of major codes to names (e.g., `{ CS: "Computer Science" }`) | + +--- + +## Logic Overview + +- Fetches available semesters with data for the selected major +- Fetches requirement coverage data for the selected major + semester +- Aggregates the number of courses per requirement (based on the last node in path) +- Uses Plotly to render a horizontal bar chart, sorted by course count +- Dynamically switches content based on data availability and loading state + +--- + +## Example Usage + +```jsx + +``` + +Used in the **Analytics** page to let users visually explore course distribution across requirement categories. You can change the major and limit by semester. + +--- + +## Related Tests + +- `CategoryCoverage.test.js` β€” test coverage includes: + - Valid/invalid API responses + - Dropdown behavior + - Chart rendering based on dynamic data + - Conditional β€œNo Data” message + +--- + +## Styling + +| Class | Description | +|-------|-------------| +| `search-container` | Wrapper layout | +| `filter-control-group` | Wrapper for dropdowns | +| `search-dropdown` | Select element styling | +| `loading-spinner` | Visual feedback during API load | +| `filter-tag` | Display fallback β€œno data” status | + +--- + +## Notes + +- Filters out requirement categories with `num_courses === 0` to reduce noise +- Responsive to container size (`useResizeHandler`) +- Resets semester when major is changed +- Requires the backend endpoint: + `GET /analytics/course-coverage?major=CS&semester=F23` + diff --git a/frontend/docs/CourseTable.md b/frontend/docs/CourseTable.md new file mode 100644 index 0000000..84cdee8 --- /dev/null +++ b/frontend/docs/CourseTable.md @@ -0,0 +1,93 @@ +# Component: `` + +## Purpose + +Renders the main table displaying filtered courses. Includes interactive filters per major, prerequisite status, and semester offerings. Each row supports a popup to show detailed course or requirement information. Also adapts behavior for β€œPlan” and β€œView” tabs. + +--- + +## Props + +| Prop Name | Type | Required | Description | +|--------------------------|----------|----------|-------------| +| `courses` | Array | βœ… | List of course objects to display | +| `allRequirements` | Object | βœ… | Requirement options grouped by major | +| `selectedFilters` | Object | βœ… | Tracks currently selected requirement filters | +| `handleFilterChange` | Function | βœ… | Updates selected filters | +| `clearFilters` | Function | βœ… | Clears all filters for a given major | +| `offeredOptions` | Array | βœ… | List of semesters used in the offered column | +| `selectedOfferedSemesters` | Array | βœ… | List of semesters currently selected | +| `setSelectedOfferedSemesters` | Function | βœ… | Updates selected offered semesters | +| `coreOnly` | Boolean | ❌ | If true, show only Core requirements | +| `genedOnly` | Boolean | ❌ | If true, show only GenEd requirements | +| `allowRemove` | Boolean | ❌ | If true, shows a delete button per course | +| `handleRemoveCourse` | Function | ❌ | Function to remove a course (only if `allowRemove`) | +| `noPrereqs` | Boolean / null | ❌ | Controls Prerequisite dropdown state | +| `setNoPrereqs` | Function | ❌ | Updates prerequisite filter setting | +| `compactViewMode` | String | ❌ | If `"last1"` or `"last2"`, trims long requirement labels | +| `hideDropdowns` | Boolean | ❌ | If true, hides filter dropdowns in headers | +| `isPlanTab` | Boolean | ❌ | If true, adjusts empty state message for Plan tab | + +--- + +## State & Logic + +- `isPopupOpen`, `popupType`, `popupContent`: control the popup modal +- `fetchCourseDetails`: fetches full details of a course on click +- `filterRequirementObjects`: filters requirements based on `coreOnly` and `genedOnly` +- `compactViewMode`: trims requirement text in cells (`last1` or `last2`) +- `OfferedCell` and `PrereqCell`: expandable cells with β€œShow more” toggles + +--- + +## Example Usage + +```jsx + +``` + +--- + +## Related Tests + +- `CourseTable.test.js` – rendering, dropdown filters, popup behavior, expand/collapse functionality. + +--- + +## Related Components + +- `` +- `` +- `` + +--- + +## Styling + +- `remove-col`: column for delete button +- `header-offered`, `header-prereq`, `header-{major}`: table header styles +- `expand-toggle`: Show more / less toggle +- `cell-offered`, `cell-prereq`, `cell-{major}`: data cell styles + +--- + +## Notes + +- Requirements are dynamically filtered and formatted based on Core/GenEd flags. +- Popup supports both course and requirement views. +- Offered semesters are sorted chronologically (F/S/M + year). +- Handles both full view and compact display modes. diff --git a/frontend/docs/CourseTablePage.md b/frontend/docs/CourseTablePage.md new file mode 100644 index 0000000..8e74db6 --- /dev/null +++ b/frontend/docs/CourseTablePage.md @@ -0,0 +1,87 @@ +# Component: `` + +## Purpose + +Acts as the main page controller for the CMU-Q GenEd frontend. Manages the UI state, filter logic, search behavior, course retrieval, and rendering of child components such as the search bar, table, and popups. + +--- + +## Internal State + +- `selectedDepartments`, `searchQuery`, `noPrereqs`, `coreOnly`, `genedOnly`, etc.: Manage user-selected filters +- `courses`, `requirements`: Store data fetched from backend +- `offeredOptions`: Populates semester dropdowns +- `sortMode`: Toggles between sorting by course code or requirements +- `compactViewMode`: Controls the display format of requirement text +- `toast`, `loading`, `showConfirmPopup`, `showClearPopup`: UI feedback and modals + +--- + +## API Calls + +- `/departments`: Fetch list of departments for dropdown +- `/requirements`: Fetch course requirement data +- `/courses/search`: Fetch filtered list of courses +- `/courses/semesters`: Get available semesters + +All filters and search values are sent as query parameters. + +--- + +## Lifecycle Effects + +- `useEffect` saves user preferences to `localStorage` for persistence +- Search query is debounced using a `setTimeout` (350ms delay) +- Courses and requirements are re-fetched when filters change +- Paginated display updates when data length or page changes + +--- + +## Handlers + +| Handler | Purpose | +|---------|---------| +| `handleFilterChange` | Updates selected filters by major | +| `clearFilters` | Clears all filters for a major | +| `removeOfferedSemester` | Removes a selected semester | +| `removePrereqFilter` | Resets the pre-req filter to null | +| `handleRemoveCourse` | Removes a course from the local course list | +| `addCoursesToPlan` | Saves selected courses to localStorage | +| `toggleSortByReqs` | Toggles between sorting modes | +| `clearAllFilters` | Resets all filters to their defaults | + +--- + +## Child Components + +- `` +- `` +- `` +- Toast and popup components for UI interaction + +--- + +## Related Tests + +- `CourseTablePage.test.js` – covers filter state logic, fetch behavior, and UI interaction +- Mocks localStorage and API calls for isolation + +--- + +## Styling + +| Class | Description | +|-------|-------------| +| `table-container` | Main container layout | +| `view-toolbar` | Toolbar with filters, sort and plan buttons | +| `toast-snackbar` | Small floating message box | +| `popup-overlay-2`, `popup-box` | Modal styles for confirmation dialogs | +| `no-results-msg` | Display when no matching courses | + +--- + +## Notes + +- Uses `AbortController` to cancel pending API fetches and avoid race conditions. +- Stores filter state in `localStorage` for persistence across page reloads. +- Modular design supports adding new filters or sorting methods with minimal refactoring. diff --git a/frontend/docs/DataUpload.md b/frontend/docs/DataUpload.md new file mode 100644 index 0000000..c524928 --- /dev/null +++ b/frontend/docs/DataUpload.md @@ -0,0 +1,60 @@ +# Component: `` + +## Purpose + +Provides a user interface to upload datasets for initializing or updating the backend database. Supports uploading course ZIPs, audit ZIPs, enrollment Excel files, and department CSV files. Performs client-side validation for structure and file type before uploading. + +--- + +## Internal State + +| State Variable | Description | +|---------------------|-------------| +| `courseZips` | ZIP files containing course data | +| `auditZips` | ZIP files containing audit requirement data | +| `enrollmentFile` | Excel file with course enrollment data | +| `departmentCsv` | CSV file with department information | +| `error` / `success` | Display messages for validation/upload outcomes | +| `loading` | Upload process status | + +--- + +## Example Usage + +```jsx + +``` + +This component can be rendered on an admin or setup page where the backend database needs to be initialized with data from structured files. Files must follow expected formats, and course data is required if audit or enrollment data is present. + +--- + +## Related Tests + +- `DataUpload.test.js` β€” verifies: + - ZIP structure validation (audit vs. course) + - File type checks for Excel/CSV + - Successful form submissions + - Error and success message handling + - Disabled state of the "Upload" button + +--- + +## Styling & UI Libraries + +| Element | Source | +|---------------|--------| +| UI Components | [MUI (Material UI)](https://mui.com) | +| File Parsing | [JSZip](https://stuk.github.io/jszip/) | +| Feedback UI | ``, `` from MUI | +| Hidden Input | Styled with `VisuallyHiddenInput` using `@mui/material/styles` | + +--- + +## Notes + +- Performs client-side file type and content validation before sending to the backend. +- Uses `FormData` to send multiple files with the same field key (e.g., multiple ZIPs). +- Backend endpoint expected: `POST /upload/init-db/` +- All validation and upload combinations are described in a detailed instructional UI inside the component. + diff --git a/frontend/docs/EnrollmentAnalytics.md b/frontend/docs/EnrollmentAnalytics.md new file mode 100644 index 0000000..411169c --- /dev/null +++ b/frontend/docs/EnrollmentAnalytics.md @@ -0,0 +1,70 @@ +# Component: `` + +## πŸ“Œ Purpose + +Displays enrollment trends for CMU-Q courses. Combines two sub-components: +- **AggregatedEnrollmentAnalytics**: shows total enrollment across semesters for multiple selected courses. +- **ClassEnrollmentAnalytics**: displays enrollment per class year (Freshman, Sophomore, etc.) for a single course. + +--- + +## πŸ”§ Internal Components + +### 1. `` + +| Feature | Description | +|-----------------|-------------| +| Input | Course code text box (`15122` or `15-122`) | +| Output | Line chart of enrollment count over semesters | +| Logic | Fetches enrollment totals for each course from `/analytics/enrollment-data` | +| UI | Adds/removes course chips, shows loading/error states | + +### 2. `` + +| Feature | Description | +|-----------------|-------------| +| Input | Single course code | +| Output | Line chart per class (1st year – 4th year) | +| Logic | Filters enrollment by `class_` field and aggregates data | +| UI | Allows toggling of view based on selected course code | + +--- + +## πŸ“€ Example Usage + +```jsx + +``` + +Place this component inside the `Analytics` tab of your app to let users explore how course enrollments evolve across semesters and cohorts. + +--- + +## πŸ§ͺ Related Tests + +- `EnrollmentAnalytics.test.js` β€” test coverage includes: + - API calls for valid/invalid course codes + - Chart rendering using mock Plotly data + - UI for search, input, loading, and error handling + +--- + +## 🎨 Styling + +| Class / Style | Description | +|-------------------|-------------| +| `search-container` | Container for each analytics panel | +| `text-input`, `add-all-btn` | Input and action controls | +| `filter-tag` | Course chips with remove buttons | +| `loading-spinner` | Displayed while fetching data | +| Plot styling | Handled by `react-plotly.js` responsive config | + +--- + +## 🚨 Notes + +- Requires backend support at: + `GET /analytics/enrollment-data?course_code=XX-XXX` +- Automatically deduplicates courses and applies semantic formatting (`15-122`) +- Enrollment traces use `lines+markers` for visual clarity +- Class-specific analytics excludes "Class 0" (unspecified or administrative entries) diff --git a/frontend/docs/MainTabs.md b/frontend/docs/MainTabs.md new file mode 100644 index 0000000..ef3fcd2 --- /dev/null +++ b/frontend/docs/MainTabs.md @@ -0,0 +1,69 @@ +# Component: `` + +## πŸ“Œ Purpose + +Serves as the entry-point navigation for the GenEd planning interface. Manages the three main tabs: **View**, **Plan**, and **Analytics**, and renders their associated components. Uses `localStorage` to persist the user’s last selected tab. + +--- + +## πŸ”§ Internal State + +| State Variable | Description | +|----------------|-------------| +| `activeTab` | Stores the currently selected tab: `"general"`, `"plan"`, or `"analytics"` | + +--- + +## 🧠 Logic Overview + +- `useEffect` stores the selected tab in `localStorage` to persist tab selection across page reloads +- Renders one of the three tab components conditionally: + - `"general"` β†’ `` + - `"plan"` β†’ `` + - `"analytics"` β†’ `` +- Applies the `"active"` class to the selected tab for styling + +--- + +## πŸ“€ Example Usage + +```jsx + +``` + +Used as the main controller in the app’s homepage or dashboard to switch between course viewing, planning, and analytics. + +--- + +## 🧱 Related Components + +- `` β€” Tab for searching and filtering courses +- `` β€” Tab for managing a planned list of courses +- `` β€” Tab for displaying data visualizations on course metrics + +--- + +## πŸ§ͺ Related Tests + +- `MainTabs.test.js` β€” test coverage includes: + - Tab switching + - Persistence using localStorage + - Conditional rendering of child components + +--- + +## 🎨 Styling + +| Class | Description | +|-------|-------------| +| `tab-bar` | Container for the tab toggle buttons | +| `tab` | Style for inactive tabs | +| `tab.active` | Highlighted tab styling | +| `tab-content` | Content area under the tab bar | + +--- + +## 🚨 Notes + +- `key` props are applied to each tab render to ensure full reset of internal state when switching. +- You can extend this component to accept custom tab content or dynamically load tabs from configuration. diff --git a/frontend/docs/MultiSelectDropdown.md b/frontend/docs/MultiSelectDropdown.md new file mode 100644 index 0000000..b119f77 --- /dev/null +++ b/frontend/docs/MultiSelectDropdown.md @@ -0,0 +1,78 @@ +# Component: `` + +## πŸ“Œ Purpose + +A reusable dropdown component supporting single or grouped multiselect behavior. Used for filtering by requirements, departments, course types, locations, and semesters in the GenEd UI. + +--- + +## πŸ”§ Props + +| Prop Name | Type | Required | Description | +|-----------------------|----------|----------|-------------| +| `major` | String | βœ… | Identifies the type of filter this dropdown manages | +| `allRequirements` | Array | βœ… | List of available options (can be flat or nested objects) | +| `selectedFilters` | Object | βœ… | Current selected filter values | +| `handleFilterChange` | Function | βœ… | Callback to apply new filter selections | +| `clearFilters` | Function | βœ… | Callback to clear all filters for this major | +| `showSelectedInButton` | Boolean | ❌ | If true, displays current selections in the button label | +| `hideSelectButtons` | Boolean | ❌ | Hides "Select All" and "Clear All" buttons if true | +| `wrapperClassName` | String | ❌ | Optional class name for styling the container | + +--- + +## 🧠 Logic Overview + +- Internally manages dropdown state (`isOpen`, `tempSelection`, `expandedGroups`) +- Uses `buildNestedGroups()` to structure Core/GenEd requirements into collapsible groups +- Dynamically renders: + - Flat checkbox lists for `department`, `location`, etc. + - Collapsible nested groups for requirement filtering +- Tracks temporary selection before applying via β€œApply Filters” button +- Includes built-in toggles for β€œSelect All”, β€œClear All”, and group-specific selection + +--- + +## πŸ“€ Example Usage + +```jsx + +``` + +You can reuse this for filtering by `department`, `offered`, `location`, `courseType`, or any requirement type. For non-requirement filters, it renders a simple list; for requirement filters, it renders a hierarchical group. + +--- + +## πŸ§ͺ Related Tests + +- `MultiSelectDropdown.test.js` β€” tests include: + - Dropdown open/close behavior + - Select/deselect logic + - Nested group toggle and β€œSelect All in group” + - Apply filter validation + +--- + +## 🎨 Styling + +| Class | Description | +|-------|-------------| +| `dropdown`, `dropdown-content`, `dropdown-btn` | Container and toggle styles | +| `dropdown-group`, `dropdown-subgroup` | Indent groups visually | +| `select-buttons-row`, `drop-apply-btn` | Action buttons within dropdown | +| `dropdown-item`, `checkbox-right` | Styles for checkbox labels | +| `dropdown-offered` | Specialized style for semester filters | + +--- + +## 🚨 Notes + +- Avoid using dropdowns with hundreds of items β€” group hierarchies can grow deep. +- Dropdown closes on outside click (via `useRef` and `mousedown` event). +- Make sure `handleFilterChange()` handles both string and array inputs depending on context. diff --git a/frontend/docs/PlanCourseTab.md b/frontend/docs/PlanCourseTab.md new file mode 100644 index 0000000..7b16761 --- /dev/null +++ b/frontend/docs/PlanCourseTab.md @@ -0,0 +1,86 @@ +# Component: `` + +## πŸ“Œ Purpose + +Provides an interactive tab where users can search for courses by code, add them to a personal plan, and view them in a dedicated table. Utilizes localStorage for persistence across sessions. + +--- + +## πŸ”§ Internal State + +| State Variable | Description | +|---------------------|-------------| +| `searchQuery` | Input for searching course code | +| `searchResults` | Results returned from course search | +| `addedCourses` | Courses added to the plan (stored in localStorage) | +| `requirements` | Requirements used for formatting the course table | +| `toast` | Snackbar message state for success/warnings | +| `showSearchResults` | Toggles visibility of search result panel | +| `compactViewMode` | Controls how requirement paths are displayed | +| `loading` | Whether requirement data is being fetched | + +--- + +## 🧠 Logic Overview + +- Uses `localStorage` to persist added courses +- Fetches requirement data from backend to support table rendering +- Enables searching by formatted course code (e.g., `15122` or `15-122`) +- Displays toast messages when adding duplicate or new courses +- Filters out already added courses from re-adding +- Supports compact requirement views (`full`, `last2`, `last1`) +- Provides a search dropdown UI that collapses on outside click + +--- + +## πŸ“€ Example Usage + +```jsx + +``` + +Used as a standalone tab or page in the GenEd planning interface. Requires the backend to expose: +- `/courses/search` +- `/courses/{course_code}` +- `/requirements` + +--- + +## 🧱 Related Components + +- `` β€” reused to render planned courses +- Toast/snackbar +- Animated search results dropdown + +--- + +## πŸ§ͺ Related Tests + +- `PlanCourseTab.test.js` β€” test coverage includes: + - Adding/removing courses + - Search result rendering + - Toast messages + - Persistence with localStorage + - UI toggle for compact views + +--- + +## 🎨 Styling + +| Class | Description | +|-------|-------------| +| `plan-tab-container` | Main wrapper layout | +| `search-bar-container`, `search-bar-enhanced` | Custom search bar layout | +| `scrollable-results` | Floating container for search matches | +| `course-result-item` | Each item in the result list | +| `toast-snackbar` | Feedback messages | +| `loading-spinner` | Loading indicator | +| `view-toggle`, `clear-all-btn` | Utility controls for view and clearing plan | + +--- + +## 🚨 Notes + +- Integrates with `CourseTable`, so format consistency and filtering logic are shared. +- Can be easily extended to allow editing/removal or viewing course details. +- This component only adds courses via search; it does not support filtering by requirements or location. diff --git a/frontend/docs/PopUp.md b/frontend/docs/PopUp.md new file mode 100644 index 0000000..b37f728 --- /dev/null +++ b/frontend/docs/PopUp.md @@ -0,0 +1,96 @@ +# Component: `` + +## Purpose + +Displays a modal overlay containing detailed information about either a course or a requirement. Dynamically switches between `course` and `requirement` view modes. Used in the CourseTable to provide interactive, in-depth exploration. + +--- + +## Props + +| Prop Name | Type | Required | Description | +|-------------|----------|----------|-------------| +| `isOpen` | Boolean | βœ… | Controls visibility of the popup | +| `onClose` | Function | βœ… | Callback for closing the popup | +| `type` | String | βœ… | "course" or "requirement" – controls rendering logic | +| `content` | Object | βœ… | Course or requirement data to display | +| `openPopup` | Function | βœ… | Used for opening nested popups (e.g., clicking a requirement course link) | + +--- + +## Logic Overview + +- `formatRequirement()`: Helper function to clean and format raw requirement strings (especially for GenEd). + - Handles variations like "General Education" or "University Core Requirements". + - Converts `---` separators into arrows (`β†’`). +- Requirements and courses are grouped and displayed per major. +- Deduplicates and sorts semesters using `sortSemesters()`. + +--- + +## Example UI + +### Course View + +Displays: +- Course code, name, units, description +- Prerequisites (cleaned) +- Semesters offered +- Requirements it fulfills (grouped by major) + +### Requirement View + +Displays: +- Formatted requirement name +- List of courses that fulfill it +- Each course is clickable and opens its popup view + +--- + + +--- + +## Example Usage + +```jsx + setIsPopupOpen(false)} + type="course" + content={selectedCourse} + openPopup={(type, content) => { + setPopupType(type); + setPopupContent(content); + setIsPopupOpen(true); + }} +/> +``` + +You can trigger this component on row clicks or requirement clicks within ``. The `type` can be `"course"` or `"requirement"` depending on the context, and the `content` prop should contain either full course data or a requirement object with a list of courses. + + +## Related Tests + +- `Popup.test.js` – test visibility toggling, correct formatting of requirements, rendering of course list + +--- + +## Styling + +| Class | Description | +|-------|-------------| +| `popup-overlay` | Full-screen dimmed background | +| `popup-panel` | Main content container | +| `popup-close-btn` | Closes the popup | +| `popup-title` | Header text | +| `requirement-group`, `requirement-list` | Styling for grouped content | +| `course-link` | Clickable course item within requirement view | + +--- + +## Notes + +- Popup is conditionally rendered – returns `null` if `isOpen` is false. +- Uses `Set` and custom sorting to handle semester display cleanly. +- Uses nested click handlers to allow opening another popup from within a requirement popup. + diff --git a/frontend/docs/SearchBar.md b/frontend/docs/SearchBar.md new file mode 100644 index 0000000..3143522 --- /dev/null +++ b/frontend/docs/SearchBar.md @@ -0,0 +1,100 @@ +# Component: `` + +## Purpose + +Provides the search and filtering interface for the GenEd course list. Supports input by course code, department selection, and dropdown filters for location and course type (Core/GenEd). State is managed via props and updates are pushed to the parent component. + +--- + +## Props + +| Prop Name | Type | Required | Description | +|--------------------|----------|----------|-------------| +| `selectedDepartments` | Array | βœ… | Selected department codes | +| `setSelectedDepartments` | Function | βœ… | Updates department filters | +| `searchQuery` | String | βœ… | Input value for course code search | +| `setSearchQuery` | Function | βœ… | Updates course code query | +| `noPrereqs` | Boolean/null | βœ… | Current prerequisite filter | +| `setNoPrereqs` | Function | βœ… | Updates pre-requisite filter | +| `offeredQatar` | Boolean/null | βœ… | Filter for Qatar campus | +| `setOfferedQatar` | Function | βœ… | Updates Qatar location filter | +| `offeredPitts` | Boolean/null | βœ… | Filter for Pittsburgh campus | +| `setOfferedPitts` | Function | βœ… | Updates Pittsburgh location filter | +| `coreOnly` | Boolean/null | βœ… | Core-only course filter | +| `setCoreOnly` | Function | βœ… | Setter for Core filter | +| `genedOnly` | Boolean/null | βœ… | GenEd-only course filter | +| `setGenedOnly` | Function | βœ… | Setter for GenEd filter | + +--- + +## Logic Overview + +- Uses `useEffect` to fetch department list from the backend. +- Default selection sets Core and GenEd to `true` if unset. +- Formats course codes via `formatCourseCode()` utility. +- Handles user interaction: + - Search by course code (supports 15122 or 15-122) + - Multi-select dropdowns for departments, campus, and course type + - Clear buttons for individual filters + +--- + +## Related Components + +- `` β€” reused for department, location, and course type + +--- + + +--- + +## Example Usage + +```jsx + +``` + +Used at the top of the course listing page to allow users to search and filter courses based on department, code, prerequisites, and location. + + +## Related Tests + +- `SearchBar.test.js` – tests for: + - Department and location dropdown rendering + - Search field updates + - Filter tags and removal logic + +--- + +## Styling + +| Class | Description | +|-------|-------------| +| `search-container` | Wrapper for the entire component | +| `search-header-row`, `search-content-row` | Organize layout into rows | +| `filter-group`, `filter-label` | Style each dropdown group | +| `text-input` | Style for the course code input field | +| `selected-filters`, `filter-tag` | Style for filter display and remove buttons | + +--- + +## Notes + +- The search is not manually triggered β€” it uses state change and `useEffect` in the parent. +- Enter key behavior is managed to blur the input but not submit anything. +- All dropdown filters support clearing and partial selection. diff --git a/frontend/docs/SelectedFilters.md b/frontend/docs/SelectedFilters.md new file mode 100644 index 0000000..b26415c --- /dev/null +++ b/frontend/docs/SelectedFilters.md @@ -0,0 +1,81 @@ +# Component: `` + +## πŸ“Œ Purpose + +Displays all currently applied filters (semesters, prerequisites, and requirement filters) as removable tags. Dynamically groups requirement tags by fully-selected groups and formats them visually. + +--- + +## πŸ”§ Props + +| Prop Name | Type | Required | Description | +|----------------------------|----------|----------|-------------| +| `selectedFilters` | Object | βœ… | Object of selected requirement filters by major | +| `handleFilterChange` | Function | βœ… | Callback to update requirement filters | +| `selectedOfferedSemesters` | Array | βœ… | List of selected semester filters | +| `removeOfferedSemester` | Function | βœ… | Callback to remove a selected semester | +| `noPrereqs` | Boolean/null | βœ… | Current state of the prerequisite filter | +| `removePrereqFilter` | Function | βœ… | Callback to reset the pre-requisite filter | +| `allRequirements` | Object | βœ… | Full list of all requirement options, grouped by major | + +--- + +## 🧠 Logic Overview + +- Uses `useMemo()` to build a nested tree of requirement groupings from `allRequirements`. +- Displays: + - Semester tags + - Prerequisite filter tag + - Fully selected requirement groups (e.g., "All Analytical Reasoning Requirements") + - Individual requirement filters +- Group tags can be removed as a batch. Individual filters can be removed independently. + +--- + +## πŸ“€ Example Usage + +```jsx + +``` + +Use this component underneath the search bar to show users which filters are active and let them remove individual or grouped filters. + +--- + +## 🧱 Related Components + +- Used in conjunction with `` and `` + +--- + +## πŸ§ͺ Related Tests + +- `SelectedFilters.test.js` β€” tests: + - Displaying correct tags + - Clicking Γ— removes individual and grouped filters + - Conditional rendering + +--- + +## 🎨 Styling + +| Class | Description | +|-------|-------------| +| `selected-filters` | Main container for all tags | +| `filter-tag` | Individual tag element | +| `filter-tag button` | Remove (Γ—) button styling | + +--- + +## 🚨 Notes + +- Group tags are automatically detected if all values in that group are selected. +- Ensures consistent formatting by trimming the last two parts of the requirement hierarchy (e.g., `GenEd β†’ Foundations β†’ Scientific Reasoning` β†’ `Foundations β†’ Scientific Reasoning`). diff --git a/frontend/docs/SingleSelectDropdown.md b/frontend/docs/SingleSelectDropdown.md new file mode 100644 index 0000000..e2b6149 --- /dev/null +++ b/frontend/docs/SingleSelectDropdown.md @@ -0,0 +1,68 @@ +# Component: `` + +## Purpose + +Provides a simple dropdown component for selecting a single value from a list of options. Used in the GenEd interface to filter by pre-requisite availability. + +--- + +## Props + +| Prop Name | Type | Required | Description | +|-------------|----------|----------|-------------| +| `options` | Array | βœ… | List of options to display in the dropdown | +| `selected` | String | βœ… | Currently selected value | +| `onChange` | Function | βœ… | Callback triggered when a new value is selected | +| `major` | String | ❌ | Optional identifier used for styling contextually (e.g., for prereq dropdown) | + +--- + +## Logic Overview + +- Internal state tracks whether the dropdown is open +- Uses `useRef` and a `mousedown` listener to close the dropdown when clicking outside +- Renders options with a radio-button-like checkbox UI (only one item can be selected at a time) + +--- + +## Example Usage + +```jsx + setNoPrereqs(value === "with" ? true : value === "without" ? false : null)} + major="prereq" +/> +``` + +Used in the β€œPrerequisites” column of `` to toggle between showing: +- All courses +- Only those with prerequisites +- Only those without prerequisites + +--- + +## Related Tests + +- `SingleSelectDropdown.test.js` – tests for: + - Opening and closing the dropdown + - Selecting an item triggers `onChange` + - Only one item can be selected at a time + +--- + +## Styling + +| Class | Description | +|-------|-------------| +| `dropdown`, `dropdown-btn`, `dropdown-content` | General dropdown styles | +| `dropdown-prereq` | Applied when `major="prereq"` to style differently | +| `dropdown-item` | Individual option style | + +--- + +## Notes + +- The dropdown is not a native `