From 2994d29d5120da03ab3383b5ef1a3800163b9a3b Mon Sep 17 00:00:00 2001 From: MC2b6 Date: Tue, 29 Oct 2024 00:18:57 -0500 Subject: [PATCH] test: profile button in header --- coverage/base.css | 224 ++++++ coverage/block-navigation.js | 87 ++ coverage/favicon.png | Bin 0 -> 445 bytes coverage/index.html | 266 +++++++ coverage/prettify.css | 1 + coverage/prettify.js | 2 + coverage/sort-arrow-sprite.png | Bin 0 -> 138 bytes coverage/sorter.js | 196 +++++ coverage/src/App.jsx.html | 304 +++++++ coverage/src/components/GroupsPage.jsx.html | 745 ++++++++++++++++++ .../components/Home/StudentFilter.jsx.html | 286 +++++++ .../src/components/Home/StudentList.jsx.html | 349 ++++++++ coverage/src/components/Home/index.html | 131 +++ coverage/src/components/HomePage.jsx.html | 427 ++++++++++ .../components/Profile/EditProfile.jsx.html | 676 ++++++++++++++++ .../components/Profile/ProfileCard.jsx.html | 343 ++++++++ .../components/Profile/ProfilePage.jsx.html | 544 +++++++++++++ .../components/Profile/SignOutDialog.jsx.html | 199 +++++ .../Profile/TimePreferencesGrid.jsx.html | 565 +++++++++++++ .../Profile/TimePreferencesPage.jsx.html | 208 +++++ .../src/components/Profile/UserCard.jsx.html | 361 +++++++++ coverage/src/components/Profile/index.html | 206 +++++ .../common/CustomPagination.jsx.html | 124 +++ .../src/components/common/Footer.jsx.html | 184 +++++ .../src/components/common/Header.jsx.html | 337 ++++++++ coverage/src/components/common/index.html | 146 ++++ coverage/src/components/index.html | 131 +++ coverage/src/hooks/auth/index.html | 131 +++ .../src/hooks/auth/useAuthNavigation.js.html | 157 ++++ coverage/src/hooks/auth/useAuthState.js.html | 151 ++++ coverage/src/hooks/data/index.html | 176 +++++ coverage/src/hooks/data/useCourses.js.html | 148 ++++ coverage/src/hooks/data/useMajors.js.html | 148 ++++ .../src/hooks/data/useStudentData.js.html | 148 ++++ .../src/hooks/data/useTimePreferences.js.html | 244 ++++++ .../src/hooks/data/useUserProfile.js.html | 253 ++++++ coverage/src/hooks/index.html | 116 +++ coverage/src/hooks/useEditProfileForm.js.html | 457 +++++++++++ coverage/src/hooks/utils/index.html | 131 +++ .../hooks/utils/useCopyToClipboard.jsx.html | 163 ++++ .../src/hooks/utils/usePagination.js.html | 136 ++++ coverage/src/index.html | 131 +++ coverage/src/index.jsx.html | 127 +++ coverage/src/utils/auth.js.html | 250 ++++++ coverage/src/utils/firebaseConfig.js.html | 178 +++++ .../src/utils/firestore/classData.js.html | 343 ++++++++ coverage/src/utils/firestore/general.js.html | 448 +++++++++++ coverage/src/utils/firestore/index.html | 161 ++++ coverage/src/utils/firestore/matches.js.html | 745 ++++++++++++++++++ .../src/utils/firestore/userProfile.js.html | 334 ++++++++ coverage/src/utils/index.html | 161 ++++ coverage/src/utils/navigateToPage.js.html | 130 +++ coverage/src/utils/theme.js.html | 139 ++++ src/App.test.jsx | 22 +- src/components/common/Header.caserio.test.jsx | 43 + vite.config.js | 3 + 56 files changed, 12801 insertions(+), 15 deletions(-) create mode 100644 coverage/base.css create mode 100644 coverage/block-navigation.js create mode 100644 coverage/favicon.png create mode 100644 coverage/index.html create mode 100644 coverage/prettify.css create mode 100644 coverage/prettify.js create mode 100644 coverage/sort-arrow-sprite.png create mode 100644 coverage/sorter.js create mode 100644 coverage/src/App.jsx.html create mode 100644 coverage/src/components/GroupsPage.jsx.html create mode 100644 coverage/src/components/Home/StudentFilter.jsx.html create mode 100644 coverage/src/components/Home/StudentList.jsx.html create mode 100644 coverage/src/components/Home/index.html create mode 100644 coverage/src/components/HomePage.jsx.html create mode 100644 coverage/src/components/Profile/EditProfile.jsx.html create mode 100644 coverage/src/components/Profile/ProfileCard.jsx.html create mode 100644 coverage/src/components/Profile/ProfilePage.jsx.html create mode 100644 coverage/src/components/Profile/SignOutDialog.jsx.html create mode 100644 coverage/src/components/Profile/TimePreferencesGrid.jsx.html create mode 100644 coverage/src/components/Profile/TimePreferencesPage.jsx.html create mode 100644 coverage/src/components/Profile/UserCard.jsx.html create mode 100644 coverage/src/components/Profile/index.html create mode 100644 coverage/src/components/common/CustomPagination.jsx.html create mode 100644 coverage/src/components/common/Footer.jsx.html create mode 100644 coverage/src/components/common/Header.jsx.html create mode 100644 coverage/src/components/common/index.html create mode 100644 coverage/src/components/index.html create mode 100644 coverage/src/hooks/auth/index.html create mode 100644 coverage/src/hooks/auth/useAuthNavigation.js.html create mode 100644 coverage/src/hooks/auth/useAuthState.js.html create mode 100644 coverage/src/hooks/data/index.html create mode 100644 coverage/src/hooks/data/useCourses.js.html create mode 100644 coverage/src/hooks/data/useMajors.js.html create mode 100644 coverage/src/hooks/data/useStudentData.js.html create mode 100644 coverage/src/hooks/data/useTimePreferences.js.html create mode 100644 coverage/src/hooks/data/useUserProfile.js.html create mode 100644 coverage/src/hooks/index.html create mode 100644 coverage/src/hooks/useEditProfileForm.js.html create mode 100644 coverage/src/hooks/utils/index.html create mode 100644 coverage/src/hooks/utils/useCopyToClipboard.jsx.html create mode 100644 coverage/src/hooks/utils/usePagination.js.html create mode 100644 coverage/src/index.html create mode 100644 coverage/src/index.jsx.html create mode 100644 coverage/src/utils/auth.js.html create mode 100644 coverage/src/utils/firebaseConfig.js.html create mode 100644 coverage/src/utils/firestore/classData.js.html create mode 100644 coverage/src/utils/firestore/general.js.html create mode 100644 coverage/src/utils/firestore/index.html create mode 100644 coverage/src/utils/firestore/matches.js.html create mode 100644 coverage/src/utils/firestore/userProfile.js.html create mode 100644 coverage/src/utils/index.html create mode 100644 coverage/src/utils/navigateToPage.js.html create mode 100644 coverage/src/utils/theme.js.html create mode 100644 src/components/common/Header.caserio.test.jsx diff --git a/coverage/base.css b/coverage/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/coverage/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/coverage/block-navigation.js b/coverage/block-navigation.js new file mode 100644 index 0000000..cc12130 --- /dev/null +++ b/coverage/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selecter that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/coverage/favicon.png b/coverage/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 22.4% + Statements + 476/2125 +
+ + +
+ 53.84% + Branches + 21/39 +
+ + +
+ 13.33% + Functions + 8/60 +
+ + +
+ 22.4% + Lines + 476/2125 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
src +
+
77.46%55/7166.66%2/350%2/477.46%55/71
src/components +
+
24.23%63/26046.15%6/1333.33%1/324.23%63/260
src/components/Home +
+
9.16%11/120100%0/00%0/29.16%11/120
src/components/Profile +
+
8.62%50/580100%4/40%0/158.62%50/580
src/components/common +
+
75%75/10025%2/840%2/575%75/100
src/hooks +
+
5.64%7/124100%0/00%0/15.64%7/124
src/hooks/auth +
+
82.6%38/46100%5/550%2/482.6%38/46
src/hooks/data +
+
26.16%45/17240%2/516.66%1/626.16%45/172
src/hooks/utils +
+
21.62%8/37100%0/00%0/221.62%8/37
src/utils +
+
53.5%61/114100%0/00%0/453.5%61/114
src/utils/firestore +
+
12.57%63/5010%0/10%0/1412.57%63/501
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/prettify.css b/coverage/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/coverage/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/prettify.js b/coverage/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/coverage/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/sort-arrow-sprite.png b/coverage/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/coverage/sorter.js b/coverage/sorter.js new file mode 100644 index 0000000..2bb296a --- /dev/null +++ b/coverage/sorter.js @@ -0,0 +1,196 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if ( + row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()) + ) { + row.style.display = ''; + } else { + row.style.display = 'none'; + } + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/coverage/src/App.jsx.html b/coverage/src/App.jsx.html new file mode 100644 index 0000000..9588b53 --- /dev/null +++ b/coverage/src/App.jsx.html @@ -0,0 +1,304 @@ + + + + + + Code coverage report for src/App.jsx + + + + + + + + + +
+
+

All files / src App.jsx

+
+ +
+ 90.16% + Statements + 55/61 +
+ + +
+ 100% + Branches + 2/2 +
+ + +
+ 66.66% + Functions + 2/3 +
+ + +
+ 90.16% + Lines + 55/61 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +741x +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +1x +1x +1x +  +1x +  +1x +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +  +1x +1x +  +1x +1x +1x +1x +1x +1x +  +1x +  +1x + 
import React, { useState } from 'react';
+ 
+import Footer from '@components/common/Footer';
+import Header from '@components/common/Header';
+import GroupsPage from '@components/GroupsPage';
+import HomePage from '@components/HomePage';
+import EditProfile from '@components/Profile/EditProfile';
+import ProfilePage from '@components/Profile/ProfilePage';
+import TimePreferencesPage from '@components/Profile/TimePreferencesPage';
+import { ThemeProvider } from '@mui/material/styles';
+import { theme } from '@utils/theme';
+import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
+import './App.css';
+ 
+const AppContent = ({ currentPage, setCurrentPage }) => {
+  const location = useLocation();
+  const isEditProfilePage = location.pathname === '/edit-profile';
+  const isHomePage = location.pathname === '/'; // Check if we are on the HomePage
+ 
+  const [isFilterOpen, setIsFilterOpen] = useState(false); // State for the filter visibility
+ 
+  const handleFilterToggle = (forceClose = null) => {
+    if (forceClose === false) {
+      setIsFilterOpen(false); // Force close the filter
+    } else {
+      setIsFilterOpen((prev) => !prev); // Toggle the filter
+    }
+  };
+ 
+  return (
+    <ThemeProvider theme={theme}>
+      {!isEditProfilePage && (
+        <Header
+          onFilterToggle={handleFilterToggle} // Pass filter toggle handler
+          isFilterOpen={isFilterOpen} // Pass filter open state
+          showFilter={isHomePage} // Only show filter button on HomePage
+        />
+      )}
+      <div className="container">
+        <Routes>
+          <Route
+            path="/"
+            element={
+              <HomePage
+                isFilterOpen={isFilterOpen}
+                handleFilterToggle={handleFilterToggle} // Pass the filter logic to HomePage
+              />
+            }
+          />
+          <Route path="groups" element={<GroupsPage />} />
+          <Route path="profile/:id" element={<ProfilePage />} />
+          <Route path="edit-profile" element={<EditProfile />} />
+          <Route path="time-preferences" element={<TimePreferencesPage />} />
+        </Routes>
+      </div>
+      {!isEditProfilePage && <Footer currentPage={currentPage} setCurrentPage={setCurrentPage} />}
+    </ThemeProvider>
+  );
+};
+ 
+const App = () => {
+  const [currentPage, setCurrentPage] = useState(0);
+ 
+  return (
+    <div className="app-wrapper">
+      <BrowserRouter>
+        <AppContent currentPage={currentPage} setCurrentPage={setCurrentPage} />
+      </BrowserRouter>
+    </div>
+  );
+};
+ 
+export default App;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/GroupsPage.jsx.html b/coverage/src/components/GroupsPage.jsx.html new file mode 100644 index 0000000..afc41ce --- /dev/null +++ b/coverage/src/components/GroupsPage.jsx.html @@ -0,0 +1,745 @@ + + + + + + Code coverage report for src/components/GroupsPage.jsx + + + + + + + + + +
+
+

All files / src/components GroupsPage.jsx

+
+ +
+ 5.38% + Statements + 9/167 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 5.38% + Lines + 9/167 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +2211x +  +1x +1x +1x +1x +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x + 
import { useState, useEffect } from 'react';
+ 
+import { useAuthState } from '@auth/useAuthState';
+import ProfileCard from '@components/Profile/ProfileCard';
+import StudentCard from '@components/Profile/UserCard';
+import useUserProfile from '@data/useUserProfile';
+import {
+  resolveMatchRequest,
+  getUserMatches,
+  removeMatch,
+  getMatchedUserUids,
+} from '@firestore/matches';
+import { fetchUserProfile } from '@firestore/userProfile';
+import { Box, Stack, Typography } from '@mui/material';
+ 
+function GroupsPage() {
+  const [user] = useAuthState();
+  const { userProfile, loading } = useUserProfile(user);
+  const [incomingRequestProfiles, setIncomingRequestProfiles] = useState([]);
+  const [outgoingRequestProfiles, setOutgoingRequestProfiles] = useState([]);
+  const [matchProfiles, setMatchProfiles] = useState([]);
+  const [selectedProfile, setSelectedProfile] = useState(null); // State for selected user profile
+  const [openProfileModal, setOpenProfileModal] = useState(false); // State for modal visibility
+ 
+  // Combined useEffect for fetching incoming, outgoing, and current matches
+  useEffect(() => {
+    const fetchRequestProfiles = async () => {
+      if (!userProfile) return;
+ 
+      try {
+        // Fetch incoming request profiles
+        const incomingProfilesPromise = Promise.all(
+          (userProfile.incomingMatches || []).map(async (req) => {
+            const { profile } = await fetchUserProfile(req.requestingUser);
+            return { ...profile, matchId: req.matchId };
+          }),
+        );
+ 
+        // Fetch outgoing request profiles
+        const outgoingProfilesPromise = Promise.all(
+          (userProfile.outgoingMatches || []).map(async (req) => {
+            const { profile } = await fetchUserProfile(req.requestedUser);
+            return profile;
+          }),
+        );
+ 
+        // Fetch current match profiles
+        const matchProfilesPromise = getUserMatches(userProfile.uid);
+ 
+        // Wait for all profiles to be fetched
+        const [incomingProfiles, outgoingProfiles, matches] = await Promise.all([
+          incomingProfilesPromise,
+          outgoingProfilesPromise,
+          matchProfilesPromise,
+        ]);
+ 
+        setIncomingRequestProfiles(incomingProfiles);
+        setOutgoingRequestProfiles(outgoingProfiles);
+        setMatchProfiles(matches);
+      } catch (error) {
+        console.error('Error fetching request profiles:', error);
+      }
+    };
+ 
+    fetchRequestProfiles();
+  }, [userProfile]);
+ 
+  const handleRequestResolution = async (requestingUserUid, matchId, accept) => {
+    try {
+      await resolveMatchRequest(userProfile.uid, requestingUserUid, matchId, accept);
+      // Update the UI after resolving the request
+      setIncomingRequestProfiles((prevProfiles) =>
+        prevProfiles.filter((profile) => profile.matchId !== matchId),
+      );
+      setMatchProfiles((prevProfiles) =>
+        prevProfiles.filter((profile) => profile.uid !== requestingUserUid),
+      );
+    } catch (error) {
+      console.error(`Error ${accept ? 'accepting' : 'denying'} request:`, error);
+    }
+  };
+ 
+  const handleRemoveMatch = async (profile) => {
+    try {
+      // Get the matched user IDs for the current user
+      const matchedUserUids = await getMatchedUserUids(userProfile.uid);
+ 
+      // Check if this profile is one of the matches
+      const sharedMatchId = profile.currentMatches.find((matchId) =>
+        matchedUserUids.includes(profile.uid),
+      );
+ 
+      if (!sharedMatchId) {
+        console.error('Match not found for the given profile');
+        return;
+      }
+ 
+      await removeMatch(sharedMatchId);
+ 
+      // Update the matchProfiles state by filtering out the removed match
+      setMatchProfiles((prevProfiles) =>
+        prevProfiles.filter((matchProfile) => matchProfile.uid !== profile.uid),
+      );
+    } catch (error) {
+      console.error('Error handling match removal:', error);
+    }
+  };
+ 
+  const handleOpenProfileModal = (profile) => {
+    setSelectedProfile(profile);
+    setOpenProfileModal(true);
+  };
+ 
+  const handleCloseProfileModal = () => {
+    setOpenProfileModal(false);
+  };
+ 
+  if (loading) {
+    return <Typography variant="h6">Loading...</Typography>;
+  }
+ 
+  if (!user) {
+    return (
+      <Box sx={{ textAlign: 'center', mt: 4 }}>
+        <Typography variant="h6" gutterBottom>
+          Please log in to view your groups.
+        </Typography>
+      </Box>
+    );
+  }
+ 
+  // Render the StudentFilter and StudentList only if the userProfile is complete
+  if (!userProfile || !userProfile.major || !userProfile.year || !userProfile.phoneNumber) {
+    return null;
+  }
+ 
+  return (
+    <Box>
+      <h1>Matches</h1>
+      <Stack spacing={2}>
+        {matchProfiles.length > 0 ? (
+          matchProfiles.map((profile, index) => {
+            const actions = [
+              {
+                label: 'View Profile',
+                onClick: () => handleOpenProfileModal(profile),
+              },
+              {
+                label: 'Unmatch',
+                onClick: () => handleRemoveMatch(profile),
+                variant: 'outlined',
+                color: 'secondary',
+              },
+            ];
+            return <StudentCard key={index} studentUserProfile={profile} actions={actions} />;
+          })
+        ) : (
+          <Typography variant="body1" color="textSecondary">
+            You don't currently have any matches.
+          </Typography>
+        )}
+      </Stack>
+ 
+      <h1>Incoming Requests</h1>
+      <Stack spacing={2}>
+        {incomingRequestProfiles.length > 0 ? (
+          incomingRequestProfiles.map((profile, index) => {
+            const actions = [
+              {
+                label: 'Accept',
+                onClick: () => handleRequestResolution(profile.uid, profile.matchId, true),
+              },
+              {
+                label: 'Deny',
+                onClick: () => handleRequestResolution(profile.uid, profile.matchId, false),
+                variant: 'outlined',
+                color: 'secondary',
+              },
+            ];
+            return <StudentCard key={index} studentUserProfile={profile} actions={actions} />;
+          })
+        ) : (
+          <Typography variant="body1" color="textSecondary">
+            You don't have any incoming requests.
+          </Typography>
+        )}
+      </Stack>
+ 
+      <h1>Outgoing Requests</h1>
+      <Stack spacing={2}>
+        {outgoingRequestProfiles.length > 0 ? (
+          outgoingRequestProfiles.map((profile, index) => {
+            const actions = [
+              {
+                label: 'Requested',
+                variant: 'outlined',
+                color: 'default',
+                onClick: () => {}, // No functionality
+              },
+            ];
+            return <StudentCard key={index} studentUserProfile={profile} actions={actions} />;
+          })
+        ) : (
+          <Typography variant="body1" color="textSecondary">
+            You don't have any outgoing requests.
+          </Typography>
+        )}
+      </Stack>
+ 
+      {/* Modal for displaying the selected profile */}
+      <ProfileCard
+        profileData={selectedProfile}
+        open={openProfileModal}
+        onClose={handleCloseProfileModal}
+      />
+    </Box>
+  );
+}
+ 
+export default GroupsPage;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Home/StudentFilter.jsx.html b/coverage/src/components/Home/StudentFilter.jsx.html new file mode 100644 index 0000000..36e899a --- /dev/null +++ b/coverage/src/components/Home/StudentFilter.jsx.html @@ -0,0 +1,286 @@ + + + + + + Code coverage report for src/components/Home/StudentFilter.jsx + + + + + + + + + +
+
+

All files / src/components/Home StudentFilter.jsx

+
+ +
+ 8.33% + Statements + 4/48 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 8.33% + Lines + 4/48 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +681x +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React from 'react';
+ 
+import useCourses from '@data/useCourses';
+import useMajors from '@data/useMajors';
+import { Box, Autocomplete, TextField } from '@mui/material';
+ 
+export default function StudentFilter({
+  selectedMajors,
+  setSelectedMajors,
+  selectedCourses,
+  setSelectedCourses,
+  selectedYears,
+  setSelectedYears,
+}) {
+  const majorsList = useMajors();
+  const coursesList = useCourses();
+ 
+  return (
+    <Box sx={{ display: { xs: 'block', md: 'flex' }, gap: 2, mb: 2 }}>
+      {/* 1. Major filter */}
+      <Autocomplete
+        multiple
+        options={majorsList}
+        getOptionLabel={(option) => option}
+        value={selectedMajors}
+        onChange={(event, newValue) => setSelectedMajors(newValue)}
+        renderInput={(params) => (
+          <TextField {...params} label="Filter by Major(s)" variant="outlined" />
+        )}
+        sx={{ flex: 1, mb: 2, minWidth: 200, maxWidth: '100%' }}
+      />
+ 
+      {/* 2. Course filter */}
+      <Autocomplete
+        multiple
+        options={coursesList}
+        getOptionLabel={(option) => option}
+        value={selectedCourses}
+        onChange={(event, newValue) => setSelectedCourses(newValue)}
+        renderInput={(params) => (
+          <TextField {...params} label="Filter by Course(s)" variant="outlined" />
+        )}
+        // filterOptions={(options, { inputValue }) => {
+        //   // Convert inputValue to lowercase for case-insensitive matching
+        //   const lowercasedInput = inputValue.toLowerCase();
+ 
+        //   // Only include options that start with the input
+        //   return options.filter((option) => option.toLowerCase().startsWith(lowercasedInput));
+        // }}
+        sx={{ flex: 1, mb: 2, minWidth: 200, maxWidth: '100%' }}
+      />
+ 
+      {/* 3. Year filter */}
+      <Autocomplete
+        multiple
+        options={['Freshman', 'Sophomore', 'Junior', 'Senior', 'Master', 'PhD']}
+        getOptionLabel={(option) => option}
+        value={selectedYears}
+        onChange={(event, newValue) => setSelectedYears(newValue)}
+        renderInput={(params) => (
+          <TextField {...params} label="Filter by Year(s)" variant="outlined" />
+        )}
+        sx={{ flex: 1, mb: 2, minWidth: 200, maxWidth: '100%' }}
+      />
+    </Box>
+  );
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Home/StudentList.jsx.html b/coverage/src/components/Home/StudentList.jsx.html new file mode 100644 index 0000000..18ab2dd --- /dev/null +++ b/coverage/src/components/Home/StudentList.jsx.html @@ -0,0 +1,349 @@ + + + + + + Code coverage report for src/components/Home/StudentList.jsx + + + + + + + + + +
+
+

All files / src/components/Home StudentList.jsx

+
+ +
+ 9.72% + Statements + 7/72 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 9.72% + Lines + 7/72 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +891x +  +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React from 'react';
+ 
+import CustomPagination from '@components/common/CustomPagination';
+import StudentCard from '@components/Profile/UserCard';
+import useStudentData from '@data/useStudentData';
+import usePagination from '@hooks/utils/usePagination';
+import { Box, Stack, Typography } from '@mui/material';
+import { createMatch } from '@utils/firestore/matches';
+ 
+export default function StudentList({
+  userProfile,
+  requestedUsers,
+  setRequestedUsers,
+  matchedUserUids,
+  selectedMajors,
+  selectedCourses,
+  selectedYears,
+}) {
+  const studentData = useStudentData();
+  const filteredStudentData = studentData?.filter((profile) => {
+    const profileCourses = profile.listOfCourses || []; // Ensure profile has courses
+    // eslint-disable-next-line max-len
+    const normalizedSelectedCourses = selectedCourses.map((course) => course.toLowerCase()); // Normalize selected courses to lowercase
+ 
+    // Ensure filtering conditions for majors, courses, and years
+    return (
+      profile.uid !== userProfile.uid && // Exclude current user
+      !matchedUserUids.has(profile.uid) && // Exclude already matched users
+      profile.major !== '' && // Ensure major is defined
+      profile.year !== '' && // Ensure year is defined
+      (selectedMajors.length === 0 || selectedMajors.includes(profile.major)) && // Filter by major
+      (selectedCourses.length === 0 || // Filter by courses (case-insensitive match)
+        profileCourses.some((course) =>
+          normalizedSelectedCourses.includes(course.toLowerCase()),
+        )) &&
+      (selectedYears.length === 0 || selectedYears.includes(profile.year)) // Filter by year
+    );
+  });
+ 
+  const {
+    currentData: studentsToDisplay,
+    currentPage,
+    totalPages,
+    handlePageChange,
+  } = usePagination(filteredStudentData || [], 10);
+ 
+  const handleMatch = async (studentUserProfile) => {
+    try {
+      await createMatch([studentUserProfile.uid, userProfile.uid], 'University Library');
+      setRequestedUsers((prev) => {
+        const newSet = new Set(prev);
+        newSet.add(studentUserProfile.uid);
+        return newSet;
+      });
+    } catch (error) {
+      console.error('Error creating match:', error);
+    }
+  };
+ 
+  return (
+    <Box>
+      {userProfile && studentsToDisplay?.length > 0 ? (
+        <Stack spacing={2}>
+          {studentsToDisplay.map((profile, index) => {
+            const requested = requestedUsers.has(profile.uid);
+            const actions = requested
+              ? [{ label: 'Requested', variant: 'outlined', color: 'default', onClick: () => {} }]
+              : [{ label: 'Match', onClick: () => handleMatch(profile) }];
+            return <StudentCard key={index} studentUserProfile={profile} actions={actions} />;
+          })}
+        </Stack>
+      ) : (
+        <Typography variant="h6" color="textSecondary" align="center">
+          No students found.
+        </Typography>
+      )}
+ 
+      {/* Custom Pagination Component */}
+      {filteredStudentData && filteredStudentData.length > 10 && (
+        <CustomPagination
+          currentPage={currentPage}
+          totalPages={totalPages}
+          onPageChange={handlePageChange}
+        />
+      )}
+    </Box>
+  );
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Home/index.html b/coverage/src/components/Home/index.html new file mode 100644 index 0000000..45b0e12 --- /dev/null +++ b/coverage/src/components/Home/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/components/Home + + + + + + + + + +
+
+

All files src/components/Home

+
+ +
+ 9.16% + Statements + 11/120 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 9.16% + Lines + 11/120 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
StudentFilter.jsx +
+
8.33%4/48100%0/00%0/18.33%4/48
StudentList.jsx +
+
9.72%7/72100%0/00%0/19.72%7/72
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/HomePage.jsx.html b/coverage/src/components/HomePage.jsx.html new file mode 100644 index 0000000..36f9cfa --- /dev/null +++ b/coverage/src/components/HomePage.jsx.html @@ -0,0 +1,427 @@ + + + + + + Code coverage report for src/components/HomePage.jsx + + + + + + + + + +
+
+

All files / src/components HomePage.jsx

+
+ +
+ 58.06% + Statements + 54/93 +
+ + +
+ 46.15% + Branches + 6/13 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 58.06% + Lines + 54/93 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +1151x +  +1x +1x +1x +1x +1x +  +1x +  +3x +  +3x +3x +3x +3x +3x +3x +3x +3x +  +  +3x +  +  +  +  +  +  +  +  +  +  +  +3x +1x +  +1x +1x +1x +  +1x +1x +1x +3x +  +3x +1x +  +  +3x +  +3x +1x +1x +1x +1x +  +1x +  +2x +2x +2x +2x +  +2x +2x +  +2x +  +3x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +3x +3x +3x +3x +3x +3x +3x +3x +3x +3x +  +3x + 
import React, { useState, useEffect, useRef } from 'react';
+ 
+import { useAuthState } from '@auth/useAuthState';
+import StudentList from '@components/Home/StudentList';
+import useUserProfile from '@data/useUserProfile';
+import { Box, CircularProgress, Typography } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+ 
+import StudentFilter from './Home/StudentFilter';
+ 
+export default function HomePage({ isFilterOpen, handleFilterToggle }) {
+  // Receive props from AppContent
+  const [user] = useAuthState();
+  const { userProfile, requestedUsers, setRequestedUsers, matchedUserUids, loading } =
+    useUserProfile(user, true);
+  const [selectedMajors, setSelectedMajors] = useState([]);
+  const [selectedCourses, setSelectedCourses] = useState([]);
+  const [selectedYears, setSelectedYears] = useState([]);
+  const filterRef = useRef(null); // Reference to the filter component
+  const navigate = useNavigate();
+ 
+  // Close the filter if clicked outside
+  const handleClickOutside = (event) => {
+    // Check if the click is inside the filter or inside the dropdown (Autocomplete or Select)
+    if (
+      filterRef.current &&
+      !filterRef.current.contains(event.target) &&
+      !document.querySelector('.MuiAutocomplete-popper')?.contains(event.target) && // For Autocomplete dropdowns
+      !document.querySelector('.MuiPopover-root')?.contains(event.target) // For Select dropdowns
+    ) {
+      handleFilterToggle(false); // Close the filter only when clicked outside
+    }
+  };
+ 
+  useEffect(() => {
+    if (isFilterOpen) {
+      document.addEventListener('mousedown', handleClickOutside); // or use 'pointerdown' instead of 'mousedown'
+    } else {
+      document.removeEventListener('mousedown', handleClickOutside);
+    }
+ 
+    return () => {
+      document.removeEventListener('mousedown', handleClickOutside); // Cleanup the listener
+    };
+  }, [isFilterOpen]);
+ 
+  useEffect(() => {
+    if (userProfile && (!userProfile.major || !userProfile.year || !userProfile.phoneNumber)) {
+      navigate('/edit-profile');
+    }
+  }, [userProfile, navigate]);
+ 
+  if (loading) {
+    return (
+      <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
+        <CircularProgress />
+      </Box>
+    );
+  }
+ 
+  if (!user) {
+    return (
+      <Box sx={{ textAlign: 'center', mt: 4 }}>
+        <Typography variant="h6" gutterBottom>
+          Please log in to access the app.
+        </Typography>
+      </Box>
+    );
+  }
+ 
+  if (!userProfile || !userProfile.major || !userProfile.year || !userProfile.phoneNumber) {
+    return null;
+  }
+ 
+  return (
+    <Box sx={{ display: 'flex', flexDirection: 'column' }}>
+      {/* Render the filter section only if it's open */}
+      {isFilterOpen && (
+        <Box
+          ref={filterRef}
+          sx={{
+            position: 'absolute',
+            top: 60,
+            left: 0,
+            zIndex: 10,
+            backgroundColor: 'white',
+            boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
+            width: '100%',
+            padding: 1,
+            marginBottom: 2,
+          }}
+        >
+          <StudentFilter
+            selectedMajors={selectedMajors}
+            setSelectedMajors={setSelectedMajors}
+            selectedCourses={selectedCourses}
+            setSelectedCourses={setSelectedCourses}
+            selectedYears={selectedYears}
+            setSelectedYears={setSelectedYears}
+          />
+        </Box>
+      )}
+      <StudentList
+        userProfile={userProfile}
+        requestedUsers={requestedUsers}
+        setRequestedUsers={setRequestedUsers}
+        matchedUserUids={matchedUserUids}
+        selectedMajors={selectedMajors}
+        selectedCourses={selectedCourses}
+        selectedYears={selectedYears}
+      />
+    </Box>
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/EditProfile.jsx.html b/coverage/src/components/Profile/EditProfile.jsx.html new file mode 100644 index 0000000..65af60e --- /dev/null +++ b/coverage/src/components/Profile/EditProfile.jsx.html @@ -0,0 +1,676 @@ + + + + + + Code coverage report for src/components/Profile/EditProfile.jsx + + + + + + + + + +
+
+

All files / src/components/Profile EditProfile.jsx

+
+ +
+ 8.6% + Statements + 13/151 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/4 +
+ + +
+ 8.6% + Lines + 13/151 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +1981x +  +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +1x + 
import React from 'react';
+ 
+import { useAuthState } from '@auth/useAuthState';
+import useCourses from '@data/useCourses';
+import useMajors from '@data/useMajors';
+import useEditProfileForm from '@hooks/useEditProfileForm';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import {
+  TextField,
+  Button,
+  CircularProgress,
+  Autocomplete,
+  IconButton,
+  MenuItem,
+  Checkbox,
+  FormLabel,
+  FormGroup,
+  FormControlLabel,
+} from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+ 
+const EditProfile = () => {
+  const [user] = useAuthState();
+  const majorsList = useMajors();
+  const coursesList = useCourses();
+  const {
+    formData,
+    selectedMajors,
+    setSelectedMajors,
+    selectedCourses,
+    setSelectedCourses,
+    handleInputChange,
+    errors,
+    isFormValid,
+    firstTimeUser,
+    loading,
+    handleSubmit,
+  } = useEditProfileForm(user);
+  const navigate = useNavigate();
+ 
+  const handleFormSubmit = async (e) => {
+    e.preventDefault();
+    const success = await handleSubmit(user.uid);
+    if (success) {
+      navigate(`/profile/${user.uid}`, { state: { fromEditProfile: true } });
+    }
+  };
+ 
+  const years = ['Freshman', 'Sophomore', 'Junior', 'Senior', 'Master', 'PhD'];
+ 
+  if (loading) return <CircularProgress />;
+ 
+  return (
+    <div style={{ position: 'relative', padding: '5px' }}>
+      {firstTimeUser && (
+        <IconButton
+          style={{ position: 'absolute', top: '2px', left: '2px', zIndex: 10 }}
+          onClick={() => navigate(`/profile/${user.uid}`, { state: { fromEditProfile: true } })}
+        >
+          <ArrowBackIcon />
+        </IconButton>
+      )}
+      <div style={{ marginTop: '40px' }}>
+        <form onSubmit={handleFormSubmit}>
+          {renderTextField('Name', 'name', formData.name, handleInputChange, errors.name)}
+          {renderTextField(
+            'Email',
+            'email',
+            formData.email,
+            handleInputChange,
+            errors.email,
+            'email',
+          )}
+          {renderTextField(
+            'Phone Number',
+            'phoneNumber',
+            formData.phoneNumber,
+            handleInputChange,
+            errors.phoneNumber,
+            'tel',
+          )}
+ 
+          {/* Major Selection */}
+          <Autocomplete
+            multiple
+            options={majorsList}
+            getOptionLabel={(option) => option}
+            value={selectedMajors}
+            onChange={(event, newValue) => {
+              if (newValue.length <= 3) setSelectedMajors(newValue);
+            }}
+            renderInput={(params) => (
+              <TextField
+                {...params}
+                label="Major"
+                error={errors.major}
+                helperText={errors.major && 'Please select your major(s)'}
+                margin="normal"
+                fullWidth
+              />
+            )}
+          />
+ 
+          {/* Course Selection */}
+          <Autocomplete
+            multiple
+            options={coursesList}
+            getOptionLabel={(option) => option}
+            value={selectedCourses}
+            onChange={(event, newValue) => setSelectedCourses(newValue)}
+            renderInput={(params) => (
+              <TextField
+                {...params}
+                label="Courses"
+                error={errors.coursesList}
+                helperText={errors.coursesList && 'Please select your course(s)'}
+                margin="normal"
+                fullWidth
+              />
+            )}
+            filterOptions={(options, { inputValue }) => {
+              // Convert inputValue to lowercase for case-insensitive matching
+              const lowercasedInput = inputValue.toLowerCase();
+ 
+              // Only include options that start with the input
+              return options.filter((option) => option.toLowerCase().startsWith(lowercasedInput));
+            }}
+            sx={{ flex: 1, minWidth: 200, maxWidth: '100%' }}
+            freeSolo
+          />
+ 
+          {/* Year Selection */}
+          {renderSelectField('Year', 'year', years, formData.year, handleInputChange, errors.year)}
+ 
+          {/* Description Field */}
+          {renderTextField('Description', 'description', formData.description, handleInputChange)}
+ 
+          {/* Location Preference */}
+          <FormLabel component="legend">Location Preference</FormLabel>
+          <FormGroup row sx={{ mb: 2 }}>
+            {renderCheckbox('In Person', 'inPerson', formData.inPerson, handleInputChange)}
+            {renderCheckbox('Online', 'online', formData.online, handleInputChange)}
+          </FormGroup>
+ 
+          <Button variant="contained" color="primary" type="submit" disabled={!isFormValid}>
+            Save Profile
+          </Button>
+        </form>
+      </div>
+    </div>
+  );
+};
+ 
+const renderTextField = (label, name, value, onChange, error = false, type = 'text') => (
+  <TextField
+    label={label}
+    name={name}
+    type={type}
+    value={value}
+    onChange={(e) => onChange(name, e.target.value)}
+    error={error}
+    helperText={error ? `${label} is required` : ''}
+    fullWidth
+    margin="normal"
+  />
+);
+ 
+const renderSelectField = (label, name, options, value, onChange, error = false) => (
+  <TextField
+    label={label}
+    name={name}
+    select
+    value={value}
+    onChange={(e) => onChange(name, e.target.value)}
+    error={error}
+    helperText={error ? `${label} is required` : ''}
+    fullWidth
+    margin="normal"
+  >
+    {options.map((option) => (
+      <MenuItem key={option} value={option}>
+        {option}
+      </MenuItem>
+    ))}
+  </TextField>
+);
+ 
+const renderCheckbox = (label, name, value, onChange) => (
+  <FormControlLabel
+    control={
+      <Checkbox name={name} checked={value} onChange={(e) => onChange(name, e.target.checked)} />
+    }
+    label={label}
+  />
+);
+ 
+export default EditProfile;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/ProfileCard.jsx.html b/coverage/src/components/Profile/ProfileCard.jsx.html new file mode 100644 index 0000000..08c575e --- /dev/null +++ b/coverage/src/components/Profile/ProfileCard.jsx.html @@ -0,0 +1,343 @@ + + + + + + Code coverage report for src/components/Profile/ProfileCard.jsx + + + + + + + + + +
+
+

All files / src/components/Profile ProfileCard.jsx

+
+ +
+ 5.71% + Statements + 4/70 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 5.71% + Lines + 4/70 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +871x +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React from 'react';
+ 
+import { useCopyToClipboard } from '@hooks/utils/useCopyToClipboard';
+import { Avatar, Box, Card, CardContent, CardHeader, Modal, Typography } from '@mui/material';
+import { useTheme, lighten } from '@mui/system';
+ 
+export default function ProfileCard({ profileData, open, onClose }) {
+  const theme = useTheme();
+  const { handleCopyToClipboard, SnackbarComponent } = useCopyToClipboard();
+ 
+  // Helper function to render profile details fields with better styling
+  const renderProfileDetail = (label, value, isCopyable = false) => (
+    <Box sx={{ mb: 1 }}>
+      <Typography
+        variant="subtitle2"
+        color="textPrimary"
+        sx={{ display: 'inline', fontWeight: 500 }}
+      >
+        {label}:
+      </Typography>{' '}
+      <Typography
+        variant="body1"
+        sx={{
+          display: 'inline',
+          color: theme.palette.text.secondary,
+          textDecoration: isCopyable ? 'underline' : 'none',
+        }}
+        onClick={() => isCopyable && value && handleCopyToClipboard(value)}
+      >
+        {value || 'N/A'}
+      </Typography>
+    </Box>
+  );
+ 
+  return (
+    <>
+      <Modal
+        open={open}
+        onClose={onClose}
+        aria-labelledby="profile-modal-title"
+        aria-describedby="profile-modal-description"
+        sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: 2 }}
+      >
+        <Card
+          sx={{
+            backgroundColor: lighten(theme.palette.primary.light, 0.8),
+            borderRadius: 2,
+            width: '100%',
+            maxWidth: 500,
+            boxShadow: 3,
+            overflow: 'hidden',
+            paddingY: 1,
+            paddingX: 3,
+          }}
+        >
+          <CardHeader
+            avatar={
+              <Avatar
+                sx={{ width: 56, height: 56 }}
+                src={profileData?.profilePic || ''}
+                alt={profileData?.name || 'Profile Picture'}
+              >
+                {profileData?.name?.[0] || ''}
+              </Avatar>
+            }
+            title={
+              <Typography variant="h5" fontWeight="600">
+                {profileData?.name || 'Unknown User'}
+              </Typography>
+            }
+          />
+          <CardContent sx={{ padding: 2 }}>
+            {renderProfileDetail('Email', profileData?.email, true)}
+            {renderProfileDetail('Phone Number', profileData?.phoneNumber, true)}
+            {renderProfileDetail('Major', profileData?.major)}
+            {renderProfileDetail('Year', profileData?.year)}
+            {renderProfileDetail('Bio', profileData?.description)}
+          </CardContent>
+        </Card>
+      </Modal>
+ 
+      {/* Snackbar for copy confirmation */}
+      <SnackbarComponent />
+    </>
+  );
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/ProfilePage.jsx.html b/coverage/src/components/Profile/ProfilePage.jsx.html new file mode 100644 index 0000000..dc264f7 --- /dev/null +++ b/coverage/src/components/Profile/ProfilePage.jsx.html @@ -0,0 +1,544 @@ + + + + + + Code coverage report for src/components/Profile/ProfilePage.jsx + + + + + + + + + +
+
+

All files / src/components/Profile ProfilePage.jsx

+
+ +
+ 9.82% + Statements + 11/112 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 9.82% + Lines + 11/112 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +1541x +  +1x +1x +  +  +  +  +  +  +  +  +  +  +  +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React, { useState } from 'react';
+ 
+import useUserProfile from '@data/useUserProfile';
+import { Email, Phone, Place, School, CalendarToday, ListAlt } from '@mui/icons-material';
+import {
+  Avatar,
+  Typography,
+  Divider,
+  Box,
+  Button,
+  Card,
+  CardContent,
+  CircularProgress,
+  Chip,
+  useTheme,
+} from '@mui/material';
+import { useParams, useNavigate } from 'react-router-dom';
+ 
+import SignOutDialog from './SignOutDialog';
+ 
+export default function ProfilePage() {
+  const { id } = useParams();
+  const navigate = useNavigate();
+  const { userProfile: profileData, loading } = useUserProfile({ uid: id });
+  const [signOutDialogOpen, setSignOutDialogOpen] = useState(false);
+ 
+  if (loading) {
+    return <CircularProgress />;
+  }
+ 
+  return (
+    <Box sx={{ maxWidth: 700, margin: 'auto', padding: 1.5 }}>
+      <ProfileHeader name={profileData?.name} profilePic={profileData?.profilePic} />
+ 
+      <InfoSection title="Contact Info">
+        <ContentBox icon={Email} title="Email" content={profileData?.email} />
+        <CustomDivider />
+        <ContentBox icon={Phone} title="Phone" content={profileData?.phoneNumber} />
+      </InfoSection>
+ 
+      <InfoSection title="Study Info">
+        <ContentBox icon={CalendarToday} title="Year" content={profileData?.year} />
+        <CustomDivider />
+        <ContentBox icon={School} title="Major" content={profileData?.major} />
+        <CustomDivider />
+        <ContentBox icon={ListAlt} title="Courses" content={profileData?.listOfCourses} isCourses />
+      </InfoSection>
+ 
+      <InfoSection title="Bio">
+        <Typography variant="body2" color="textSecondary">
+          {profileData?.description}
+        </Typography>
+      </InfoSection>
+ 
+      <InfoSection title="Preferences">
+        <ContentBox
+          icon={Place}
+          title="Location"
+          content={
+            <Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
+              {profileData?.locationPreference.inPerson && (
+                <Chip label="In Person" variant="filled" />
+              )}
+              {profileData?.locationPreference.online && <Chip label="Online" variant="filled" />}
+            </Box>
+          }
+        />
+      </InfoSection>
+ 
+      <ActionButtons
+        onEditClick={() => navigate('/edit-profile')}
+        onTimePreferencesClick={() => navigate('/time-preferences')}
+        onSignOutClick={() => setSignOutDialogOpen(true)}
+      />
+      <SignOutDialog open={signOutDialogOpen} onClose={() => setSignOutDialogOpen(false)} />
+    </Box>
+  );
+}
+ 
+const ProfileHeader = ({ name, profilePic }) => {
+  const theme = useTheme();
+  return (
+    <Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column', mb: 4 }}>
+      <Avatar
+        sx={{ width: 100, height: 100, bgcolor: theme.palette.primary.main, mb: 2 }}
+        src={profilePic || ''}
+        alt={name}
+      >
+        {name?.[0]}
+      </Avatar>
+      <Typography variant="h4" sx={{ fontWeight: 'bold' }}>
+        {name}
+      </Typography>
+    </Box>
+  );
+};
+ 
+const InfoSection = ({ title, children }) => (
+  <>
+    <Typography variant="subtitle1" sx={{ fontWeight: 'bold', mb: 1 }}>
+      {title}
+    </Typography>
+    <Card variant="outlined" sx={{ mb: 3, borderRadius: 2 }}>
+      <CardContent>{children}</CardContent>
+    </Card>
+  </>
+);
+ 
+const ContentBox = ({ icon: IconComponent, title, content, isCourses = false }) => (
+  <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+    <Box sx={{ display: 'flex', alignItems: 'center' }}>
+      <IconComponent sx={{ mr: 1, color: 'grey.600' }} />
+      <Typography variant="body2" color="textSecondary" sx={{ mr: 1 }}>
+        {title}
+      </Typography>
+    </Box>
+    {isCourses && Array.isArray(content) ? (
+      <Box
+        sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, ml: 'auto', justifyContent: 'flex-end' }}
+      >
+        {content.map((course, index) => (
+          <Chip key={index} label={course} />
+        ))}
+      </Box>
+    ) : (
+      <Typography variant="body2">{content}</Typography>
+    )}
+  </Box>
+);
+ 
+const CustomDivider = () => <Divider sx={{ my: 1 }} />;
+ 
+const ActionButtons = ({ onEditClick, onTimePreferencesClick, onSignOutClick }) => (
+  <Box
+    sx={{
+      display: 'flex',
+      justifyContent: 'center',
+      flexDirection: 'column',
+      alignItems: 'center',
+      mt: 4,
+    }}
+  >
+    <Button variant="contained" sx={{ mb: 1, width: '250px' }} onClick={onEditClick}>
+      Edit Profile
+    </Button>
+    <Button variant="contained" sx={{ mb: 3, width: '250px' }} onClick={onTimePreferencesClick}>
+      Time Preferences
+    </Button>
+    <Button variant="contained" color="secondary" sx={{ width: '150px' }} onClick={onSignOutClick}>
+      Sign Out
+    </Button>
+  </Box>
+);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/SignOutDialog.jsx.html b/coverage/src/components/Profile/SignOutDialog.jsx.html new file mode 100644 index 0000000..74be725 --- /dev/null +++ b/coverage/src/components/Profile/SignOutDialog.jsx.html @@ -0,0 +1,199 @@ + + + + + + Code coverage report for src/components/Profile/SignOutDialog.jsx + + + + + + + + + +
+
+

All files / src/components/Profile SignOutDialog.jsx

+
+ +
+ 16.66% + Statements + 4/24 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 16.66% + Lines + 4/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +391x +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React from 'react';
+ 
+import {
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogContentText,
+  DialogTitle,
+  Button,
+} from '@mui/material';
+import { handleSignOut } from '@utils/auth';
+import { useNavigate } from 'react-router-dom';
+ 
+export default function SignOutDialog({ open, onClose }) {
+  const navigate = useNavigate();
+ 
+  const confirmSignOut = () => {
+    handleSignOut(navigate);
+    onClose();
+  };
+ 
+  return (
+    <Dialog open={open} onClose={onClose}>
+      <DialogTitle>Confirm Sign Out</DialogTitle>
+      <DialogContent>
+        <DialogContentText>Are you sure you want to sign out?</DialogContentText>
+      </DialogContent>
+      <DialogActions>
+        <Button onClick={onClose} color="primary">
+          Cancel
+        </Button>
+        <Button onClick={confirmSignOut} color="secondary">
+          Sign Out
+        </Button>
+      </DialogActions>
+    </Dialog>
+  );
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/TimePreferencesGrid.jsx.html b/coverage/src/components/Profile/TimePreferencesGrid.jsx.html new file mode 100644 index 0000000..f86869b --- /dev/null +++ b/coverage/src/components/Profile/TimePreferencesGrid.jsx.html @@ -0,0 +1,565 @@ + + + + + + Code coverage report for src/components/Profile/TimePreferencesGrid.jsx + + + + + + + + + +
+
+

All files / src/components/Profile TimePreferencesGrid.jsx

+
+ +
+ 8.13% + Statements + 10/123 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 8.13% + Lines + 10/123 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +1611x +  +  +  +  +  +  +  +  +  +  +  +  +1x +  +  +1x +24x +24x +24x +  +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x + 
import React, { useState } from 'react';
+ 
+import {
+  Box,
+  Paper,
+  Typography,
+  Select,
+  MenuItem,
+  FormControl,
+  InputLabel,
+  Switch,
+  FormControlLabel,
+  useTheme,
+} from '@mui/material';
+ 
+// Generate 12-hour clock with AM/PM
+const hours = Array.from({ length: 24 }, (_, i) => {
+  const hour = i % 12 || 12;
+  return `${hour} ${i < 12 ? 'AM' : 'PM'}`;
+});
+ 
+const weekdays = ['M', 'T', 'W', 'Th', 'F'];
+const weekendDays = ['Sa', 'Su'];
+ 
+const TimePreferencesGrid = ({ selectedTimes, setSelectedTimes }) => {
+  const [noEarlierThan, setNoEarlierThan] = useState(8); // Time index for "No earlier than"
+  const [noLaterThan, setNoLaterThan] = useState(20); // Time index for "No later than"
+  const [includeWeekends, setIncludeWeekends] = useState(false); // Toggle for showing weekends
+ 
+  const handleSlotClick = (day, hour) => {
+    const slot = `${day}-${hour}`;
+    setSelectedTimes((prev) =>
+      prev.includes(slot) ? prev.filter((time) => time !== slot) : [...prev, slot],
+    );
+  };
+ 
+  const theme = useTheme();
+ 
+  // Handle filtering the displayed hours based on the selected limits
+  const filteredHours = hours.slice(noEarlierThan, noLaterThan + 1);
+ 
+  return (
+    <Box sx={{ maxWidth: 800, margin: 'auto', padding: 3 }}>
+      {/* No Earlier Than and No Later Than selectors */}
+      <Box sx={{ display: 'flex', justifyContent: 'space-between', marginBottom: 2 }}>
+        <FormControl sx={{ minWidth: 120 }}>
+          <InputLabel>No earlier than</InputLabel>
+          <Select
+            value={noEarlierThan}
+            label="No earlier than"
+            onChange={(e) => setNoEarlierThan(e.target.value)}
+          >
+            {hours.map((hour, index) => (
+              <MenuItem key={hour} value={index}>
+                {hour}
+              </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+ 
+        <FormControl sx={{ minWidth: 120 }}>
+          <InputLabel>No later than</InputLabel>
+          <Select
+            value={noLaterThan}
+            label="No later than"
+            onChange={(e) => setNoLaterThan(e.target.value)}
+          >
+            {hours.map((hour, index) => (
+              <MenuItem key={hour} value={index}>
+                {hour}
+              </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+      </Box>
+ 
+      {/* Toggle for Weekends */}
+      <Box sx={{ display: 'flex', justifyContent: 'center', marginBottom: 2 }}>
+        <FormControlLabel
+          control={
+            <Switch
+              checked={includeWeekends}
+              onChange={(e) => setIncludeWeekends(e.target.checked)}
+              color="primary"
+            />
+          }
+          label="Include Weekends"
+        />
+      </Box>
+ 
+      {/* Time Grid */}
+      <Box
+        sx={{
+          display: 'grid',
+          gridTemplateColumns: `repeat(${includeWeekends ? weekdays.length + weekendDays.length + 1 : weekdays.length + 1}, 1fr)`,
+          gap: 1,
+        }}
+      >
+        <Box /> {/* Empty cell for alignment */}
+        {weekdays.map((day) => (
+          <Typography key={day} variant="subtitle1" align="center">
+            {day}
+          </Typography>
+        ))}
+        {includeWeekends &&
+          weekendDays.map((day) => (
+            <Typography key={day} variant="subtitle1" align="center">
+              {day}
+            </Typography>
+          ))}
+        {filteredHours.map((hour) => (
+          <React.Fragment key={hour}>
+            <Typography variant="subtitle2" align="center" sx={{ whiteSpace: 'nowrap' }}>
+              {hour}
+            </Typography>
+            {weekdays.map((day) => {
+              const slot = `${day}-${hour}`;
+              const isSelected = selectedTimes.includes(slot);
+ 
+              return (
+                <Paper
+                  key={slot}
+                  onClick={() => handleSlotClick(day, hour)}
+                  sx={{
+                    width: '100%',
+                    height: 30,
+                    backgroundColor: isSelected ? theme.palette.primary.main : 'white',
+                    cursor: 'pointer',
+                    border: '0.5px solid grey',
+                  }}
+                />
+              );
+            })}
+            {includeWeekends &&
+              weekendDays.map((day) => {
+                const slot = `${day}-${hour}`;
+                const isSelected = selectedTimes.includes(slot);
+ 
+                return (
+                  <Paper
+                    key={slot}
+                    onClick={() => handleSlotClick(day, hour)}
+                    sx={{
+                      width: '100%',
+                      height: 30,
+                      backgroundColor: isSelected ? theme.palette.primary.main : 'white',
+                      cursor: 'pointer',
+                      border: '0.5px solid grey',
+                    }}
+                  />
+                );
+              })}
+          </React.Fragment>
+        ))}
+      </Box>
+    </Box>
+  );
+};
+ 
+export default React.memo(TimePreferencesGrid);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/TimePreferencesPage.jsx.html b/coverage/src/components/Profile/TimePreferencesPage.jsx.html new file mode 100644 index 0000000..4451bc4 --- /dev/null +++ b/coverage/src/components/Profile/TimePreferencesPage.jsx.html @@ -0,0 +1,208 @@ + + + + + + Code coverage report for src/components/Profile/TimePreferencesPage.jsx + + + + + + + + + +
+
+

All files / src/components/Profile TimePreferencesPage.jsx

+
+ +
+ 14.81% + Statements + 4/27 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 14.81% + Lines + 4/27 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +421x +  +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React from 'react';
+ 
+import useTimePreferences from '@data/useTimePreferences';
+import { Box, Typography, Button, CircularProgress } from '@mui/material';
+ 
+import TimePreferencesGrid from './TimePreferencesGrid';
+ 
+export default function TimePreferencesPage() {
+  const { selectedTimes, setSelectedTimes, loading, savePreferences } = useTimePreferences();
+ 
+  // Function to save preferences and navigate back to profile page
+  const handleSavePreferences = async () => {
+    await savePreferences();
+  };
+ 
+  if (loading) {
+    return (
+      <Box
+        sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}
+      >
+        <CircularProgress />
+      </Box>
+    );
+  }
+ 
+  return (
+    <Box sx={{ maxWidth: 800, margin: 'auto', padding: 3, alignItems: 'center' }}>
+      <Typography variant="h4" align="center" gutterBottom>
+        Time Preferences
+      </Typography>
+ 
+      <TimePreferencesGrid selectedTimes={selectedTimes} setSelectedTimes={setSelectedTimes} />
+ 
+      <Box sx={{ display: 'flex', justifyContent: 'center', marginTop: 2 }}>
+        <Button variant="contained" onClick={handleSavePreferences}>
+          Save Preferences
+        </Button>
+      </Box>
+    </Box>
+  );
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/UserCard.jsx.html b/coverage/src/components/Profile/UserCard.jsx.html new file mode 100644 index 0000000..12459d0 --- /dev/null +++ b/coverage/src/components/Profile/UserCard.jsx.html @@ -0,0 +1,361 @@ + + + + + + Code coverage report for src/components/Profile/UserCard.jsx + + + + + + + + + +
+
+

All files / src/components/Profile UserCard.jsx

+
+ +
+ 5.47% + Statements + 4/73 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 5.47% + Lines + 4/73 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +931x +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import {
+  Card,
+  CardHeader,
+  CardContent,
+  CardActions,
+  Typography,
+  Avatar,
+  Button,
+} from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { lighten } from '@mui/system';
+ 
+export default function StudentCard({ studentUserProfile, actions }) {
+  const theme = useTheme();
+  return (
+    <Card
+      sx={{
+        backgroundColor: lighten(theme.palette.primary.light, 0.8),
+        borderRadius: '16px', //  to make the card rounder
+      }}
+    >
+      <CardHeader
+        sx={{
+          pb: 0,
+          display: 'flex',
+          alignItems: 'flex-start',
+        }}
+        avatar={
+          <Avatar
+            sx={{ marginTop: '-4px', bgcolor: '#4E2A84', width: 56, height: 56 }}
+            src={studentUserProfile?.profilePic || ''} // Use Google profile picture if available
+            alt={studentUserProfile?.name}
+          >
+            {!studentUserProfile?.photoURL && (studentUserProfile?.name?.[0] || '')}{' '}
+            {/* Display student's initial if for some reason the Google photo URL was not stored */}
+          </Avatar>
+        }
+        title={
+          <Typography
+            variant="h5"
+            component="h2"
+            sx={{
+              color: 'rgba(0, 0, 0, 0.85)',
+              fontWeight: '540',
+              fontSize: '1.6rem',
+              lineHeight: 1.2,
+            }}
+          >
+            {studentUserProfile.name}
+          </Typography>
+        }
+        subheader={
+          <>
+            <Typography color="textSecondary" component="h6">
+              {studentUserProfile.major} ({studentUserProfile.year})
+            </Typography>
+          </>
+        }
+      />
+      <CardContent>
+        {studentUserProfile.listOfCourses.length > 0 &&
+        studentUserProfile.listOfCourses[0] !== '' ? (
+          <Typography color="textSecondary" component="h6">
+            Courses: {studentUserProfile.listOfCourses.join(',')}
+          </Typography>
+        ) : null}
+        <Typography color="textSecondary">{studentUserProfile.description}</Typography>
+      </CardContent>
+      {actions ? (
+        <CardActions sx={{ justifyContent: 'flex-end' }}>
+          {actions.map((action, index) => (
+            <Button
+              key={index}
+              size="small"
+              variant={action.variant || 'contained'}
+              color={action.color || 'primary'}
+              onClick={action.onClick}
+              sx={{
+                borderRadius: '12px',
+                margin: 2,
+              }} // This makes the button rounder
+            >
+              {action.label}
+            </Button>
+          ))}
+        </CardActions>
+      ) : (
+        <></>
+      )}
+    </Card>
+  );
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/Profile/index.html b/coverage/src/components/Profile/index.html new file mode 100644 index 0000000..f65a3e3 --- /dev/null +++ b/coverage/src/components/Profile/index.html @@ -0,0 +1,206 @@ + + + + + + Code coverage report for src/components/Profile + + + + + + + + + +
+
+

All files src/components/Profile

+
+ +
+ 8.62% + Statements + 50/580 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 0% + Functions + 0/15 +
+ + +
+ 8.62% + Lines + 50/580 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
EditProfile.jsx +
+
8.6%13/151100%0/00%0/48.6%13/151
ProfileCard.jsx +
+
5.71%4/70100%0/00%0/15.71%4/70
ProfilePage.jsx +
+
9.82%11/112100%0/00%0/69.82%11/112
SignOutDialog.jsx +
+
16.66%4/24100%0/00%0/116.66%4/24
TimePreferencesGrid.jsx +
+
8.13%10/123100%4/40%0/18.13%10/123
TimePreferencesPage.jsx +
+
14.81%4/27100%0/00%0/114.81%4/27
UserCard.jsx +
+
5.47%4/73100%0/00%0/15.47%4/73
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/common/CustomPagination.jsx.html b/coverage/src/components/common/CustomPagination.jsx.html new file mode 100644 index 0000000..ea66488 --- /dev/null +++ b/coverage/src/components/common/CustomPagination.jsx.html @@ -0,0 +1,124 @@ + + + + + + Code coverage report for src/components/common/CustomPagination.jsx + + + + + + + + + +
+
+

All files / src/components/common CustomPagination.jsx

+
+ +
+ 44.44% + Statements + 4/9 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 44.44% + Lines + 4/9 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +141x +  +1x +  +1x +  +  +  +  +  +  +  +1x + 
import React from 'react';
+ 
+import { Box, Pagination } from '@mui/material';
+ 
+const CustomPagination = ({ currentPage, totalPages, onPageChange }) => {
+  return (
+    <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
+      <Pagination count={totalPages} page={currentPage} onChange={onPageChange} color="primary" />
+    </Box>
+  );
+};
+ 
+export default CustomPagination;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/common/Footer.jsx.html b/coverage/src/components/common/Footer.jsx.html new file mode 100644 index 0000000..d46f225 --- /dev/null +++ b/coverage/src/components/common/Footer.jsx.html @@ -0,0 +1,184 @@ + + + + + + Code coverage report for src/components/common/Footer.jsx + + + + + + + + + +
+
+

All files / src/components/common Footer.jsx

+
+ +
+ 88% + Statements + 22/25 +
+ + +
+ 100% + Branches + 1/1 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 88% + Lines + 22/25 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +341x +  +1x +1x +1x +1x +1x +1x +  +  +  +1x +1x +1x +  +1x +1x +1x +1x +1x +1x +  +  +  +1x +  +1x +1x +  +1x +1x +  +1x + 
import React from 'react';
+ 
+import GroupsIcon from '@mui/icons-material/Groups';
+import HomeIcon from '@mui/icons-material/Home';
+import { Box, BottomNavigation, BottomNavigationAction } from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { navigateToPage } from '@utils/navigateToPage';
+import { useNavigate } from 'react-router-dom';
+ 
+// import MessageIcon from '@mui/icons-material/Message';
+ 
+export default function Footer({ currentPage, setCurrentPage }) {
+  const navigate = useNavigate();
+  const theme = useTheme();
+ 
+  return (
+    <Box className="footer">
+      <BottomNavigation
+        showLabels
+        value={currentPage}
+        onChange={(event, newValue) => {
+          setCurrentPage(newValue);
+          navigateToPage(navigate, newValue);
+        }}
+        sx={{ backgroundColor: theme.palette.primary.light }}
+      >
+        <BottomNavigationAction label="Home" icon={<HomeIcon />} />
+        <BottomNavigationAction label="Matches" icon={<GroupsIcon />} />
+        {/* <BottomNavigationAction label="Messages" icon={<MessageIcon />} /> */}
+      </BottomNavigation>
+    </Box>
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/common/Header.jsx.html b/coverage/src/components/common/Header.jsx.html new file mode 100644 index 0000000..67bcd21 --- /dev/null +++ b/coverage/src/components/common/Header.jsx.html @@ -0,0 +1,337 @@ + + + + + + Code coverage report for src/components/common/Header.jsx + + + + + + + + + +
+
+

All files / src/components/common Header.jsx

+
+ +
+ 74.24% + Statements + 49/66 +
+ + +
+ 14.28% + Branches + 1/7 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 74.24% + Lines + 49/66 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +851x +  +1x +1x +1x +1x +1x +1x +  +1x +1x +1x +1x +1x +  +1x +1x +1x +  +  +  +1x +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +1x +  +1x +  +  +  +  +1x +  +1x +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +1x +  +1x +1x +  +  +  +  +1x +  +1x +  +  +  +  +1x +1x +  +1x + 
import React from 'react';
+ 
+import { useAuthNavigation } from '@auth/useAuthNavigation';
+import { ArrowBack } from '@mui/icons-material';
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import { AppBar, Toolbar, Typography, IconButton, Avatar, Button, Box } from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { useNavigate, useLocation } from 'react-router-dom';
+ 
+export default function Header({ onFilterToggle, isFilterOpen, showFilter }) {
+  const { user, handleProfileClick, signInAndCheckFirstTimeUser } = useAuthNavigation();
+  const theme = useTheme();
+  const navigate = useNavigate();
+  const location = useLocation();
+ 
+  const isProfilePage = location.pathname.includes('/profile/');
+  const isRootPage =
+    location.pathname === '/' ||
+    location.pathname === '/groups' ||
+    location.pathname === '/messages';
+ 
+  const handleBackButtonClick = () => {
+    if (location.state?.fromEditProfile) {
+      navigate('/'); // Redirect to the home page
+    } else if (window.history.length > 2) {
+      navigate(-1); // Go back to the previous page in the history stack if available
+    } else {
+      navigate('/'); // Otherwise, redirect to the home page
+    }
+  };
+ 
+  return (
+    <AppBar position="sticky" sx={{ backgroundColor: theme.palette.primary.light, color: '#000' }}>
+      <Toolbar sx={{ position: 'relative', justifyContent: 'space-between' }}>
+        <Box sx={{ display: 'flex', alignItems: 'center' }}>
+          {showFilter && (
+            <IconButton
+              edge="start"
+              color="inherit"
+              onClick={onFilterToggle}
+              sx={{ marginRight: 1 }}
+            >
+              <FilterAltIcon color={isFilterOpen ? 'secondary' : 'inherit'} />
+            </IconButton>
+          )}
+          {!isRootPage ? (
+            <IconButton edge="start" color="inherit" onClick={handleBackButtonClick}>
+              <ArrowBack />
+            </IconButton>
+          ) : (
+            <Box sx={{ width: '48px' }} /> // Placeholder if no back button is needed
+          )}
+        </Box>
+ 
+        <Typography
+          variant="h6"
+          sx={{
+            position: 'absolute',
+            left: '50%',
+            transform: 'translateX(-50%)',
+            fontWeight: '600',
+            fontSize: '1.4rem',
+          }}
+        >
+          StudyBuddy
+        </Typography>
+ 
+        {!isProfilePage ? (
+          user ? (
+            <IconButton edge="end" color="inherit" onClick={handleProfileClick}>
+              <Avatar alt={user.displayName} src={user.photoURL} />
+            </IconButton>
+          ) : (
+            <Button color="inherit" onClick={signInAndCheckFirstTimeUser}>
+              Sign In
+            </Button>
+          )
+        ) : (
+          <Box sx={{ width: '48px' }} />
+        )}
+      </Toolbar>
+    </AppBar>
+  );
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/common/index.html b/coverage/src/components/common/index.html new file mode 100644 index 0000000..d9e388c --- /dev/null +++ b/coverage/src/components/common/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for src/components/common + + + + + + + + + +
+
+

All files src/components/common

+
+ +
+ 75% + Statements + 75/100 +
+ + +
+ 25% + Branches + 2/8 +
+ + +
+ 40% + Functions + 2/5 +
+ + +
+ 75% + Lines + 75/100 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
CustomPagination.jsx +
+
44.44%4/9100%0/00%0/144.44%4/9
Footer.jsx +
+
88%22/25100%1/150%1/288%22/25
Header.jsx +
+
74.24%49/6614.28%1/750%1/274.24%49/66
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/components/index.html b/coverage/src/components/index.html new file mode 100644 index 0000000..f4cc7ed --- /dev/null +++ b/coverage/src/components/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/components + + + + + + + + + +
+
+

All files src/components

+
+ +
+ 24.23% + Statements + 63/260 +
+ + +
+ 46.15% + Branches + 6/13 +
+ + +
+ 33.33% + Functions + 1/3 +
+ + +
+ 24.23% + Lines + 63/260 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
GroupsPage.jsx +
+
5.38%9/167100%0/00%0/15.38%9/167
HomePage.jsx +
+
58.06%54/9346.15%6/1350%1/258.06%54/93
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/auth/index.html b/coverage/src/hooks/auth/index.html new file mode 100644 index 0000000..ed546cb --- /dev/null +++ b/coverage/src/hooks/auth/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/hooks/auth + + + + + + + + + +
+
+

All files src/hooks/auth

+
+ +
+ 82.6% + Statements + 38/46 +
+ + +
+ 100% + Branches + 5/5 +
+ + +
+ 50% + Functions + 2/4 +
+ + +
+ 82.6% + Lines + 38/46 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useAuthNavigation.js +
+
66.66%16/24100%1/133.33%1/366.66%16/24
useAuthState.js +
+
100%22/22100%4/4100%1/1100%22/22
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/auth/useAuthNavigation.js.html b/coverage/src/hooks/auth/useAuthNavigation.js.html new file mode 100644 index 0000000..6c116da --- /dev/null +++ b/coverage/src/hooks/auth/useAuthNavigation.js.html @@ -0,0 +1,157 @@ + + + + + + Code coverage report for src/hooks/auth/useAuthNavigation.js + + + + + + + + + +
+
+

All files / src/hooks/auth useAuthNavigation.js

+
+ +
+ 66.66% + Statements + 16/24 +
+ + +
+ 100% + Branches + 1/1 +
+ + +
+ 33.33% + Functions + 1/3 +
+ + +
+ 66.66% + Lines + 16/24 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +251x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +1x +1x +1x +  +  +  +  +  +1x +1x +1x +1x + 
import { useAuthState } from '@auth/useAuthState';
+import { handleSignIn } from '@utils/auth';
+import { useNavigate } from 'react-router-dom';
+ 
+export const useAuthNavigation = () => {
+  const [user] = useAuthState();
+  const navigate = useNavigate();
+ 
+  const handleProfileClick = () => {
+    if (user) {
+      navigate(`/profile/${user.uid}`);
+    }
+  };
+ 
+  const signInAndCheckFirstTimeUser = () => {
+    handleSignIn().then((user) => {
+      if (!user) {
+        navigate('/edit-profile');
+      }
+    });
+  };
+ 
+  return { user, handleProfileClick, signInAndCheckFirstTimeUser };
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/auth/useAuthState.js.html b/coverage/src/hooks/auth/useAuthState.js.html new file mode 100644 index 0000000..db4b48c --- /dev/null +++ b/coverage/src/hooks/auth/useAuthState.js.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for src/hooks/auth/useAuthState.js + + + + + + + + + +
+
+

All files / src/hooks/auth useAuthState.js

+
+ +
+ 100% + Statements + 22/22 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 22/22 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +231x +1x +1x +1x +1x +1x +1x +4x +4x +4x +4x +2x +2x +2x +2x +2x +2x +2x +4x +4x +4x +4x + 
import { useEffect, useState } from 'react';
+ 
+import { auth } from '@utils/firebaseConfig';
+import { onAuthStateChanged } from 'firebase/auth';
+ 
+// Hook to get the current user
+export const useAuthState = () => {
+  const [user, setUser] = useState(null);
+  const [error, setError] = useState(null);
+ 
+  useEffect(() => {
+    const unsubscribe = onAuthStateChanged(
+      auth,
+      (user) => setUser(user),
+      (error) => setError(error),
+    );
+ 
+    return () => unsubscribe(); // cleanup on unmount
+  }, []);
+ 
+  return [user, error];
+};
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/data/index.html b/coverage/src/hooks/data/index.html new file mode 100644 index 0000000..501854d --- /dev/null +++ b/coverage/src/hooks/data/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for src/hooks/data + + + + + + + + + +
+
+

All files src/hooks/data

+
+ +
+ 26.16% + Statements + 45/172 +
+ + +
+ 40% + Branches + 2/5 +
+ + +
+ 16.66% + Functions + 1/6 +
+ + +
+ 26.16% + Lines + 45/172 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useCourses.js +
+
23.8%5/21100%0/00%0/123.8%5/21
useMajors.js +
+
23.8%5/21100%0/00%0/123.8%5/21
useStudentData.js +
+
23.8%5/21100%0/00%0/123.8%5/21
useTimePreferences.js +
+
16.98%9/53100%0/00%0/116.98%9/53
useUserProfile.js +
+
37.5%21/5640%2/550%1/237.5%21/56
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/data/useCourses.js.html b/coverage/src/hooks/data/useCourses.js.html new file mode 100644 index 0000000..22d0045 --- /dev/null +++ b/coverage/src/hooks/data/useCourses.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for src/hooks/data/useCourses.js + + + + + + + + + +
+
+

All files / src/hooks/data useCourses.js

+
+ +
+ 23.8% + Statements + 5/21 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 23.8% + Lines + 5/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +221x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { useEffect, useState } from 'react';
+ 
+import { getCourses } from '@firestore/general';
+ 
+export default function useCourses() {
+  const [CoursesList, setCoursesList] = useState([]);
+
+  useEffect(() => {
+    const fetchCourses = async () => {
+      try {
+        const Courses = await getCourses();
+        setCoursesList(Courses);
+      } catch (error) {
+        console.error('Error fetching Courses:', error);
+      }
+    };
+    fetchCourses();
+  }, []);
+
+  return CoursesList;
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/data/useMajors.js.html b/coverage/src/hooks/data/useMajors.js.html new file mode 100644 index 0000000..3627bb3 --- /dev/null +++ b/coverage/src/hooks/data/useMajors.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for src/hooks/data/useMajors.js + + + + + + + + + +
+
+

All files / src/hooks/data useMajors.js

+
+ +
+ 23.8% + Statements + 5/21 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 23.8% + Lines + 5/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +221x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { useEffect, useState } from 'react';
+ 
+import { getMajors } from '@firestore/general';
+ 
+export default function useMajors() {
+  const [majorsList, setMajorsList] = useState([]);
+
+  useEffect(() => {
+    const fetchMajors = async () => {
+      try {
+        const majors = await getMajors();
+        setMajorsList(majors);
+      } catch (error) {
+        console.error('Error fetching majors:', error);
+      }
+    };
+    fetchMajors();
+  }, []);
+
+  return majorsList;
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/data/useStudentData.js.html b/coverage/src/hooks/data/useStudentData.js.html new file mode 100644 index 0000000..c33fd75 --- /dev/null +++ b/coverage/src/hooks/data/useStudentData.js.html @@ -0,0 +1,148 @@ + + + + + + Code coverage report for src/hooks/data/useStudentData.js + + + + + + + + + +
+
+

All files / src/hooks/data useStudentData.js

+
+ +
+ 23.8% + Statements + 5/21 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 23.8% + Lines + 5/21 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +221x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { useEffect, useState } from 'react';
+ 
+import { getAllUsers } from '@firestore/general';
+ 
+export default function useStudentData() {
+  const [studentData, setStudentData] = useState(null);
+
+  useEffect(() => {
+    const fetchStudentData = async () => {
+      try {
+        const students = await getAllUsers();
+        setStudentData(students);
+      } catch (error) {
+        console.error('Error fetching student data:', error);
+      }
+    };
+    fetchStudentData();
+  }, []);
+
+  return studentData;
+}
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/data/useTimePreferences.js.html b/coverage/src/hooks/data/useTimePreferences.js.html new file mode 100644 index 0000000..a56ed07 --- /dev/null +++ b/coverage/src/hooks/data/useTimePreferences.js.html @@ -0,0 +1,244 @@ + + + + + + Code coverage report for src/hooks/data/useTimePreferences.js + + + + + + + + + +
+
+

All files / src/hooks/data useTimePreferences.js

+
+ +
+ 16.98% + Statements + 9/53 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 16.98% + Lines + 9/53 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +541x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x + 
import { useState, useEffect } from 'react';
+ 
+import { useAuthState } from '@auth/useAuthState';
+import { fetchUserProfile, updateUserProfile } from '@firestore/userProfile';
+import { useNavigate } from 'react-router-dom';
+ 
+const useTimePreferences = () => {
+  // Initialize as null to indicate loading state
+  const [selectedTimes, setSelectedTimes] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [user] = useAuthState();
+  const navigate = useNavigate();
+
+  const userId = user?.uid;
+
+  // Set selected times from user profile only when userProfile is updated
+  useEffect(() => {
+    const loadPreferences = async () => {
+      try {
+        const { profile } = await fetchUserProfile(userId);
+        const fetchedTimes = profile?.timePreferences || [];
+        setSelectedTimes(fetchedTimes);
+      } catch (err) {
+        console.error('Failed to load time preferences');
+      } finally {
+        setLoading(false);
+      }
+    };
+
+    loadPreferences();
+  }, [userId]);
+
+  // Function to save the selected time preferences
+  const savePreferences = async () => {
+    try {
+      await updateUserProfile(userId, {
+        timePreferences: selectedTimes,
+      });
+      navigate(`/profile/${userId}`);
+    } catch (err) {
+      console.error('Failed to save time preferences');
+    }
+  };
+
+  return {
+    selectedTimes,
+    setSelectedTimes,
+    loading: loading,
+    savePreferences,
+  };
+};
+ 
+export default useTimePreferences;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/data/useUserProfile.js.html b/coverage/src/hooks/data/useUserProfile.js.html new file mode 100644 index 0000000..2131470 --- /dev/null +++ b/coverage/src/hooks/data/useUserProfile.js.html @@ -0,0 +1,253 @@ + + + + + + Code coverage report for src/hooks/data/useUserProfile.js + + + + + + + + + +
+
+

All files / src/hooks/data useUserProfile.js

+
+ +
+ 37.5% + Statements + 21/56 +
+ + +
+ 40% + Branches + 2/5 +
+ + +
+ 50% + Functions + 1/2 +
+ + +
+ 37.5% + Lines + 21/56 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +571x +1x +1x +1x +1x +1x +3x +3x +3x +3x +3x +3x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +3x +3x +3x +3x + 
import { useEffect, useState } from 'react';
+ 
+import { getMatchedUserUids } from '@firestore/matches';
+import { fetchUserProfile } from '@firestore/userProfile';
+ 
+export default function useUserProfile(user, getMatchData = false) {
+  const [userProfile, setUserProfile] = useState(null);
+  const [requestedUsers, setRequestedUsers] = useState(new Set());
+  const [matchedUserUids, setMatchedUserUids] = useState(new Set());
+  const [loading, setLoading] = useState(true);
+ 
+  useEffect(() => {
+    if (user?.uid) {
+      const fetchUserProfileData = async () => {
+        try {
+          // Fetch user profile using the unified function
+          const { profile } = await fetchUserProfile(user.uid);
+
+          if (profile) {
+            setUserProfile(profile);
+
+            // If getMatchData is false, skip fetching match data
+            if (!getMatchData) {
+              setLoading(false);
+              return;
+            }
+
+            if (profile.major === '' || profile.year === '') {
+              setLoading(false);
+              return;
+            }
+
+            setRequestedUsers(new Set(profile.outgoingMatches.map((match) => match.requestedUser)));
+
+            const matchedUids = await getMatchedUserUids(user.uid);
+            setMatchedUserUids(new Set(matchedUids));
+          } else {
+            // Handle the case where profile is not found
+            console.warn('User profile does not exist.');
+            setUserProfile(null);
+          }
+        } catch (error) {
+          console.error('Error fetching user profile:', error);
+        }
+        setLoading(false);
+      };
+
+      fetchUserProfileData();
+    } else {
+      setUserProfile(null);
+      setLoading(false);
+    }
+  }, [user?.uid, getMatchData]);
+ 
+  return { userProfile, requestedUsers, setRequestedUsers, matchedUserUids, loading };
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/index.html b/coverage/src/hooks/index.html new file mode 100644 index 0000000..5fde3f7 --- /dev/null +++ b/coverage/src/hooks/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for src/hooks + + + + + + + + + +
+
+

All files src/hooks

+
+ +
+ 5.64% + Statements + 7/124 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 5.64% + Lines + 7/124 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useEditProfileForm.js +
+
5.64%7/124100%0/00%0/15.64%7/124
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/useEditProfileForm.js.html b/coverage/src/hooks/useEditProfileForm.js.html new file mode 100644 index 0000000..c20b722 --- /dev/null +++ b/coverage/src/hooks/useEditProfileForm.js.html @@ -0,0 +1,457 @@ + + + + + + Code coverage report for src/hooks/useEditProfileForm.js + + + + + + + + + +
+
+

All files / src/hooks useEditProfileForm.js

+
+ +
+ 5.64% + Statements + 7/124 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 5.64% + Lines + 7/124 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +1251x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x + 
import { useEffect, useState } from 'react';
+ 
+import { fetchUserProfile, updateUserProfile } from '@firestore/userProfile';
+ 
+const useEditProfileForm = (user) => {
+  const [loading, setLoading] = useState(true);
+  const [selectedMajors, setSelectedMajors] = useState([]);
+  const [selectedCourses, setSelectedCourses] = useState([]);
+  const [formData, setFormData] = useState({
+    name: '',
+    email: '',
+    phoneNumber: '',
+    major: '',
+    year: '',
+    description: '',
+    listOfCourses: '',
+    inPerson: false,
+    online: false,
+  });
+
+  const [errors, setErrors] = useState({});
+  const [isFormValid, setIsFormValid] = useState(false);
+  const [firstTimeUser, setFirstTimeUser] = useState(false);
+
+  useEffect(() => {
+    const fetchProfileMajorsAndCourses = async () => {
+      if (user && user.uid) {
+        try {
+          const { profile: data } = await fetchUserProfile(user.uid);
+          if (data) {
+            setFormData({
+              name: data.name || '',
+              email: data.email || '',
+              phoneNumber: data.phoneNumber || '',
+              major: data.major || '',
+              year: data.year || '',
+              description: data.description || '',
+              inPerson: data.locationPreference.inPerson || false,
+              online: data.locationPreference.online || false,
+            });
+            setSelectedMajors(data.major ? data.major.split(',') : []);
+            setSelectedCourses(data.listOfCourses || []);
+            setFirstTimeUser(!!(data.major && data.year));
+          }
+        } catch (error) {
+          console.error('Error fetching profile, majors, and courses:', error);
+        } finally {
+          setLoading(false);
+        }
+      }
+    };
+    fetchProfileMajorsAndCourses();
+  }, [user]);
+
+  useEffect(() => {
+    validateForm();
+  }, [formData, selectedMajors, selectedCourses]);
+
+  const handleInputChange = (name, value) => {
+    if (name === 'phoneNumber') {
+      // Format phone number as (XXX)-XXX-XXXX
+      let formattedValue = value.replace(/\D/g, ''); // Remove all non-numeric characters
+      if (formattedValue.length > 3 && formattedValue.length <= 6) {
+        formattedValue = `(${formattedValue.slice(0, 3)})-${formattedValue.slice(3)}`;
+      } else if (formattedValue.length > 6) {
+        formattedValue = `(${formattedValue.slice(0, 3)})-${formattedValue.slice(3, 6)}-${formattedValue.slice(6, 10)}`;
+      } else if (formattedValue.length > 0) {
+        formattedValue = `(${formattedValue}`;
+      }
+      setFormData({ ...formData, [name]: formattedValue });
+    } else {
+      setFormData({ ...formData, [name]: value });
+    }
+  };
+
+  const validateForm = () => {
+    const newErrors = {
+      name: !formData.name,
+      email: !/^\S+@\S+\.\S+$/.test(formData.email),
+      phoneNumber: formData.phoneNumber.replace(/\D/g, '').length !== 10,
+      major: selectedMajors.length === 0,
+      coursesList: selectedCourses.length === 0,
+      year: !formData.year,
+    };
+    setErrors(newErrors);
+    setIsFormValid(!Object.values(newErrors).some((error) => error));
+  };
+
+  const handleSubmit = async (userId) => {
+    if (isFormValid && userId) {
+      try {
+        const updatedProfileData = {
+          ...formData,
+          major: selectedMajors.join(', '),
+          listOfCourses: selectedCourses,
+          locationPreference: { inPerson: formData.inPerson, online: formData.online },
+        };
+        await updateUserProfile(userId, updatedProfileData);
+        return true; // Indicate success
+      } catch (error) {
+        console.error('Error updating profile:', error);
+        return false; // Indicate failure
+      }
+    }
+    return false; // Indicate failure if form is not valid
+  };
+
+  return {
+    formData,
+    setFormData,
+    selectedMajors,
+    setSelectedMajors,
+    selectedCourses,
+    setSelectedCourses,
+    handleInputChange,
+    errors,
+    isFormValid,
+    firstTimeUser,
+    loading,
+    handleSubmit,
+  };
+};
+ 
+export default useEditProfileForm;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/utils/index.html b/coverage/src/hooks/utils/index.html new file mode 100644 index 0000000..35ddf11 --- /dev/null +++ b/coverage/src/hooks/utils/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src/hooks/utils + + + + + + + + + +
+
+

All files src/hooks/utils

+
+ +
+ 21.62% + Statements + 8/37 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/2 +
+ + +
+ 21.62% + Lines + 8/37 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
useCopyToClipboard.jsx +
+
15%3/20100%0/00%0/115%3/20
usePagination.js +
+
29.41%5/17100%0/00%0/129.41%5/17
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/utils/useCopyToClipboard.jsx.html b/coverage/src/hooks/utils/useCopyToClipboard.jsx.html new file mode 100644 index 0000000..ceb71eb --- /dev/null +++ b/coverage/src/hooks/utils/useCopyToClipboard.jsx.html @@ -0,0 +1,163 @@ + + + + + + Code coverage report for src/hooks/utils/useCopyToClipboard.jsx + + + + + + + + + +
+
+

All files / src/hooks/utils useCopyToClipboard.jsx

+
+ +
+ 15% + Statements + 3/20 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 15% + Lines + 3/20 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +271x +  +1x +  +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { useState } from 'react';
+ 
+import { Snackbar } from '@mui/material';
+ 
+export const useCopyToClipboard = () => {
+  const [snackbarOpen, setSnackbarOpen] = useState(false);
+  const [snackbarMessage, setSnackbarMessage] = useState('');
+ 
+  const handleCopyToClipboard = (text) => {
+    navigator.clipboard.writeText(text);
+    setSnackbarMessage(`Copied to clipboard: ${text}`);
+    setSnackbarOpen(true);
+  };
+ 
+  const SnackbarComponent = () => (
+    <Snackbar
+      open={snackbarOpen}
+      autoHideDuration={3000}
+      onClose={() => setSnackbarOpen(false)}
+      message={snackbarMessage}
+      anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+    />
+  );
+ 
+  return { handleCopyToClipboard, SnackbarComponent };
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/hooks/utils/usePagination.js.html b/coverage/src/hooks/utils/usePagination.js.html new file mode 100644 index 0000000..dc16069 --- /dev/null +++ b/coverage/src/hooks/utils/usePagination.js.html @@ -0,0 +1,136 @@ + + + + + + Code coverage report for src/hooks/utils/usePagination.js + + + + + + + + + +
+
+

All files / src/hooks/utils usePagination.js

+
+ +
+ 29.41% + Statements + 5/17 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 29.41% + Lines + 5/17 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +181x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x + 
import { useState } from 'react';
+ 
+const usePagination = (data = [], itemsPerPage = 10) => {
+  const [currentPage, setCurrentPage] = useState(1);
+
+  const totalPages = Math.ceil(data.length / itemsPerPage);
+
+  const currentData = data.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
+
+  const handlePageChange = (event, page) => {
+    setCurrentPage(page);
+  };
+
+  return { currentData, currentPage, totalPages, handlePageChange };
+};
+ 
+export default usePagination;
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/index.html b/coverage/src/index.html new file mode 100644 index 0000000..310accd --- /dev/null +++ b/coverage/src/index.html @@ -0,0 +1,131 @@ + + + + + + Code coverage report for src + + + + + + + + + +
+
+

All files src

+
+ +
+ 77.46% + Statements + 55/71 +
+ + +
+ 66.66% + Branches + 2/3 +
+ + +
+ 50% + Functions + 2/4 +
+ + +
+ 77.46% + Lines + 55/71 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
App.jsx +
+
90.16%55/61100%2/266.66%2/390.16%55/61
index.jsx +
+
0%0/100%0/10%0/10%0/10
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/index.jsx.html b/coverage/src/index.jsx.html new file mode 100644 index 0000000..1154058 --- /dev/null +++ b/coverage/src/index.jsx.html @@ -0,0 +1,127 @@ + + + + + + Code coverage report for src/index.jsx + + + + + + + + + +
+
+

All files / src index.jsx

+
+ +
+ 0% + Statements + 0/10 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/10 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import React from 'react';
+ 
+import ReactDOM from 'react-dom/client';
+ 
+import './index.css';
+import App from './App';
+ 
+const root = ReactDOM.createRoot(document.getElementById('root'));
+ 
+root.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
+);
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/auth.js.html b/coverage/src/utils/auth.js.html new file mode 100644 index 0000000..2c0bd6b --- /dev/null +++ b/coverage/src/utils/auth.js.html @@ -0,0 +1,250 @@ + + + + + + Code coverage report for src/utils/auth.js + + + + + + + + + +
+
+

All files / src/utils auth.js

+
+ +
+ 29.09% + Statements + 16/55 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 29.09% + Lines + 16/55 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +561x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  + 
// import { fetchAndStoreClassData } from '@firestore/classData';
+import { createFirstUserProfile, fetchUserProfile } from '@firestore/userProfile';
+import { auth } from '@utils/firebaseConfig';
+import { GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth';
+ 
+const provider = new GoogleAuthProvider();
+ 
+// Google Sign-In
+const signInWithGoogle = async () => {
+  try {
+    const result = await signInWithPopup(auth, provider);
+    return result.user;
+  } catch (error) {
+    console.error('Error during sign-in:', error);
+    return null;
+  }
+};
+ 
+// Handle Sign-In
+//     Return true: user already exists in the database
+export const handleSignIn = async () => {
+  const user = await signInWithGoogle();
+
+  if (user) {
+    const { profile } = await fetchUserProfile(user.uid);
+
+    if (!profile) {
+      await createFirstUserProfile(user);
+      return false;
+    }
+    // ! TEMPORARY REMOVE: Fetch and store class data
+    // TODO: uncomment this code after the demo
+
+    // const res = await fetchAndStoreClassData();
+    // console.clear();
+    // if (res) {
+    //   console.warn('Class data fetched and stored:', res);
+    // } else {
+    //   console.warn('Classes not update:', res);
+    // }
+    return true;
+  }
+  return false;
+};
+ 
+// Handle Sign-Out
+export const handleSignOut = async (navigate) => {
+  try {
+    await signOut(auth);
+    console.log('Sign out successful');
+    navigate('/');
+  } catch (error) {
+    console.error('Error during sign-out:', error);
+  }
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/firebaseConfig.js.html b/coverage/src/utils/firebaseConfig.js.html new file mode 100644 index 0000000..3d97ed6 --- /dev/null +++ b/coverage/src/utils/firebaseConfig.js.html @@ -0,0 +1,178 @@ + + + + + + Code coverage report for src/utils/firebaseConfig.js + + + + + + + + + +
+
+

All files / src/utils firebaseConfig.js

+
+ +
+ 100% + Statements + 26/26 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 26/26 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +321x +1x +  +  +  +  +  +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + 
import { initializeApp } from 'firebase/app';
+import { getAuth } from 'firebase/auth';
+import {
+  CACHE_SIZE_UNLIMITED,
+  initializeFirestore,
+  persistentLocalCache,
+  persistentSingleTabManager,
+} from 'firebase/firestore';
+ 
+const firebaseConfig = {
+  apiKey: 'AIzaSyAtUHyFNDDtuPVwmH9mNCXa6o2qE_kF6OA',
+  authDomain: 'studybuddy-8086e.firebaseapp.com',
+  projectId: 'studybuddy-8086e',
+  storageBucket: 'studybuddy-8086e.appspot.com',
+  messagingSenderId: '677938268288',
+  appId: '1:677938268288:web:b74725ffd461455c76be65',
+  measurementId: 'G-EKR2SD2LE8',
+};
+ 
+// Initialize Firebase
+const app = initializeApp(firebaseConfig);
+ 
+// Enable IndexedDB persistence with single-tab manager
+export const db = initializeFirestore(app, {
+  localCache: persistentLocalCache({
+    tabManager: persistentSingleTabManager(),
+    cacheSizeBytes: CACHE_SIZE_UNLIMITED,
+  }),
+});
+ 
+export const auth = getAuth(app);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/firestore/classData.js.html b/coverage/src/utils/firestore/classData.js.html new file mode 100644 index 0000000..ad48303 --- /dev/null +++ b/coverage/src/utils/firestore/classData.js.html @@ -0,0 +1,343 @@ + + + + + + Code coverage report for src/utils/firestore/classData.js + + + + + + + + + +
+
+

All files / src/utils/firestore classData.js

+
+ +
+ 0% + Statements + 0/86 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 0% + Lines + 0/86 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// Functions for fetching and storing class data in Firestore
+import { db } from '@utils/firebaseConfig';
+import { collection, doc, getDoc, getDocs, setDoc, updateDoc } from 'firebase/firestore';
+
+// Get latest term from APi and update if necessary
+const checkAndInsertLatestTerm = async () => {
+  try {
+    // Gather latest term data from API
+    const TermResponse = await fetch('https://api.dilanxd.com/paper/data');
+    if (!TermResponse.ok) throw new Error(`Failed to fetch data: ${TermResponse.statusText}`);
+
+    const TermData = await TermResponse.json();
+    const LatestTermID = TermData.latest;
+    if (!LatestTermID) throw new Error(`'latest' term not found in initial data`);
+
+    const LatestTermName = TermData.terms[LatestTermID].name;
+
+    // Reference Firestore document
+    const termDocRef = doc(collection(db, 'majorsCourses'), 'latestTerm');
+    const termSnapshot = await getDoc(termDocRef);
+
+    const termData = termSnapshot.exists() ? termSnapshot.data() : null;
+    // Insert or update term if necessary
+    if (!termData || termData.latestTermID !== LatestTermID) {
+      const newTermData = { latestTermID: LatestTermID, latestTermName: LatestTermName };
+      termSnapshot.exists()
+        ? await updateDoc(termDocRef, newTermData)
+        : await setDoc(termDocRef, newTermData);
+    } else {
+      throw new NoUpdateError('Latest term already exists in Firestore');
+    }
+
+    return LatestTermID;
+  } catch (error) {
+    throw new Error(`Error handling latest term: ${error}`);
+  }
+};
+
+// Gathering class data from API and storing in Firestore
+export const fetchAndStoreClassData = async () => {
+  try {
+    const LatestTermID = await checkAndInsertLatestTerm();
+    if (!LatestTermID) return false;
+
+    const ClassResponse = await fetch(`https://cdn.dil.sh/paper-data/${LatestTermID}.json`);
+    if (!ClassResponse.ok) {
+      throw new Error(`Failed to fetch class data: ${ClassResponse.statusText}`);
+    }
+
+    const ClassData = await ClassResponse.json();
+
+    // Create a map to store subjects and their associated numbers
+    const subjectMap = new Map();
+
+    // Iterate over ClassData and build subject-number pairs
+    ClassData.forEach((classItem) => {
+      const { u: subject, n: number } = classItem;
+
+      // If the subject already exists in the map, append the number to its array
+      if (subjectMap.has(subject)) {
+        subjectMap.get(subject).push(number);
+      } else {
+        // Otherwise, create a new entry for this subject with the number
+        subjectMap.set(subject, [number]);
+      }
+    });
+
+    // Clear the existing courseData collection in Firestore
+    const courseDataRef = collection(db, 'courseData');
+    const courseDataSnapshot = await getDocs(courseDataRef);
+    courseDataSnapshot.forEach(async (doc) => {
+      await setDoc(doc.ref, { numbers: [] });
+    });
+
+    // Iterate over the subjectMap and store each subject with its numbers in Firestore
+    for (const [subject, numbers] of subjectMap) {
+      const subjectDocRef = doc(collection(db, 'courseData'), subject);
+      await setDoc(subjectDocRef, { numbers });
+    }
+
+    return true;
+  } catch (error) {
+    console.error('Error fetching or saving data:', error);
+    return false;
+  }
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/firestore/general.js.html b/coverage/src/utils/firestore/general.js.html new file mode 100644 index 0000000..e997193 --- /dev/null +++ b/coverage/src/utils/firestore/general.js.html @@ -0,0 +1,448 @@ + + + + + + Code coverage report for src/utils/firestore/general.js + + + + + + + + + +
+
+

All files / src/utils/firestore general.js

+
+ +
+ 15.7% + Statements + 19/121 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/3 +
+ + +
+ 15.7% + Lines + 19/121 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +1221x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
// General Firestore functions (shared utilities)
+import { db } from '@utils/firebaseConfig';
+import { collection, getDocs, doc, getDoc, onSnapshot } from 'firebase/firestore';
+ 
+// Caches for Firestore data
+const usersCache = new Map();
+const majorsCache = { data: null, timestamp: 0 };
+const coursesCache = { data: new Set(), timestamp: 0 };
+// Cache time-to-live (TTL) of 5 days
+const cacheTTL = 5 * 24 * 3600 * 1000;
+ 
+// Get all users from Firestore with caching and real-time updates
+export const getAllUsers = async () => {
+  if (usersCache.size > 0) {
+    return Array.from(usersCache.values());
+  }
+
+  try {
+    const usersCollectionRef = collection(db, 'users');
+    const usersSnapshot = await getDocs(usersCollectionRef);
+
+    usersSnapshot.docs.forEach((doc) => {
+      usersCache.set(doc.id, doc.data()); // Cache the user data
+    });
+
+    // Set up real-time listener to update cache
+    onSnapshot(usersCollectionRef, (snapshot) => {
+      snapshot.docChanges().forEach((change) => {
+        if (change.type === 'added' || change.type === 'modified') {
+          usersCache.set(change.doc.id, change.doc.data());
+        } else if (change.type === 'removed') {
+          usersCache.delete(change.doc.id);
+        }
+      });
+    });
+
+    return Array.from(usersCache.values());
+  } catch (error) {
+    console.error('Error fetching user profiles:', error);
+    return [];
+  }
+};
+ 
+// Get list of majors from Firestore with caching and real-time updates
+export const getMajors = async () => {
+  const now = Date.now();
+  if (majorsCache.data && now - majorsCache.timestamp < cacheTTL) {
+    return majorsCache.data;
+  }
+
+  try {
+    const majorsDocRef = doc(collection(db, 'majorsCourses'), 'majors');
+    const majorsSnapshot = await getDoc(majorsDocRef);
+    if (majorsSnapshot.exists()) {
+      const majorsData = majorsSnapshot.data().majors;
+      majorsCache.data = majorsData;
+      majorsCache.timestamp = now;
+
+      // Set up real-time listener to update cache
+      onSnapshot(majorsDocRef, (docSnapshot) => {
+        if (docSnapshot.exists()) {
+          majorsCache.data = docSnapshot.data().majors;
+          majorsCache.timestamp = Date.now();
+        }
+      });
+
+      return majorsData;
+    }
+    return [];
+  } catch (error) {
+    console.error('Error fetching majors:', error);
+    return [];
+  }
+};
+ 
+// Get list of courses from Firestore and store them as a Set with caching and real-time updates
+export const getCourses = async () => {
+  const now = Date.now();
+  if (coursesCache.data.size > 0 && now - coursesCache.timestamp < cacheTTL) {
+    return Array.from(coursesCache.data);
+  }
+
+  try {
+    const coursesCollectionRef = collection(db, 'courseData');
+    const coursesSnapshot = await getDocs(coursesCollectionRef);
+
+    coursesSnapshot.docs.forEach((doc) => {
+      const subject = doc.id;
+      const numbers = doc.data().numbers || [];
+      numbers.forEach((courseNumber) => {
+        coursesCache.data.add(`${subject} ${courseNumber}`);
+      });
+    });
+
+    coursesCache.timestamp = now;
+
+    // Set up real-time listener to update cache
+    onSnapshot(coursesCollectionRef, (snapshot) => {
+      snapshot.docChanges().forEach((change) => {
+        const subject = change.doc.id;
+        const numbers = change.doc.data().numbers || [];
+
+        if (change.type === 'added' || change.type === 'modified') {
+          numbers.forEach((courseNumber) => {
+            coursesCache.data.add(`${subject} ${courseNumber}`);
+          });
+        } else if (change.type === 'removed') {
+          numbers.forEach((courseNumber) => {
+            coursesCache.data.delete(`${subject} ${courseNumber}`);
+          });
+        }
+      });
+      coursesCache.timestamp = Date.now();
+    });
+
+    return Array.from(coursesCache.data);
+  } catch (error) {
+    console.error('Error fetching courses:', error);
+    return [];
+  }
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/firestore/index.html b/coverage/src/utils/firestore/index.html new file mode 100644 index 0000000..6283ede --- /dev/null +++ b/coverage/src/utils/firestore/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for src/utils/firestore + + + + + + + + + +
+
+

All files src/utils/firestore

+
+ +
+ 12.57% + Statements + 63/501 +
+ + +
+ 0% + Branches + 0/1 +
+ + +
+ 0% + Functions + 0/14 +
+ + +
+ 12.57% + Lines + 63/501 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
classData.js +
+
0%0/860%0/10%0/10%0/86
general.js +
+
15.7%19/121100%0/00%0/315.7%19/121
matches.js +
+
9.95%21/211100%0/00%0/69.95%21/211
userProfile.js +
+
27.71%23/83100%0/00%0/427.71%23/83
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/firestore/matches.js.html b/coverage/src/utils/firestore/matches.js.html new file mode 100644 index 0000000..130e16c --- /dev/null +++ b/coverage/src/utils/firestore/matches.js.html @@ -0,0 +1,745 @@ + + + + + + Code coverage report for src/utils/firestore/matches.js + + + + + + + + + +
+
+

All files / src/utils/firestore matches.js

+
+ +
+ 9.95% + Statements + 21/211 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/6 +
+ + +
+ 9.95% + Lines + 21/211 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +2211x +1x +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
import { fetchUserProfile } from '@firestore/userProfile';
+import { db } from '@utils/firebaseConfig';
+import {
+  query,
+  where,
+  getDocs,
+  doc,
+  runTransaction,
+  arrayUnion,
+  arrayRemove,
+  collection,
+} from 'firebase/firestore';
+ 
+// Batch fetch match documents using getDocs by match IDs
+const fetchMatchDocuments = async (matchIds) => {
+  try {
+    const q = query(collection(db, 'matches'), where('__name__', 'in', matchIds));
+    const querySnapshot = await getDocs(q);
+
+    // Return all match data from Firestore
+    return querySnapshot.docs.map((doc) => doc.data());
+  } catch (error) {
+    console.error('Error fetching match documents:', error);
+    return [];
+  }
+};
+ 
+// Create a new match
+export const createMatch = async (users, location, description = '') => {
+  if (!Array.isArray(users) || users.length < 2) {
+    throw new Error('Invalid user list');
+  }
+  if (!location) {
+    throw new Error('Missing required match details');
+  }
+
+  const usersWithStatus = users.map((userId, index) => ({
+    uid: userId,
+    status: index === 0 ? 'confirmed' : 'pending', // First user is confirmed
+    joinedAt: new Date().toISOString(),
+  }));
+
+  try {
+    const matchRef = doc(collection(db, 'matches'));
+    await runTransaction(db, async (transaction) => {
+      const userProfiles = await Promise.all(
+        usersWithStatus.map((user) => fetchUserProfile(user.uid, transaction)),
+      );
+
+      // Set the match document
+      transaction.set(matchRef, {
+        users: usersWithStatus,
+        location,
+        description,
+        awaitingConfirmation: false,
+        createdAt: new Date().toISOString(),
+      });
+
+      // Update user match references
+      transaction.update(userProfiles[0].ref, {
+        incomingMatches: arrayUnion({
+          requestingUser: userProfiles[1].profile.uid,
+          matchId: matchRef.id,
+        }),
+      });
+
+      transaction.update(userProfiles[1].ref, {
+        outgoingMatches: arrayUnion({
+          requestedUser: userProfiles[0].profile.uid,
+          matchId: matchRef.id,
+        }),
+      });
+    });
+
+    console.log('Match created with ID: ', matchRef.id);
+    return matchRef.id;
+  } catch (error) {
+    console.error('Error creating match:', error);
+  }
+};
+ 
+// Remove an existing match
+export const removeMatch = async (matchId) => {
+  if (!matchId) {
+    throw new Error('Missing match ID');
+  }
+
+  const matchRef = doc(db, 'matches', matchId);
+
+  try {
+    await runTransaction(db, async (transaction) => {
+      const matchDoc = await transaction.get(matchRef);
+
+      if (!matchDoc.exists()) {
+        throw new Error('Match not found');
+      }
+
+      const matchData = matchDoc.data();
+      const user0 = matchData.users[0];
+      const user1 = matchData.users[1];
+
+      // Fetch the user profiles from Firestore
+      const user0Ref = doc(db, 'users', user0.uid);
+      const user1Ref = doc(db, 'users', user1.uid);
+
+      // Remove the match reference from both users
+      transaction.update(user0Ref, {
+        currentMatches: arrayRemove(matchId),
+      });
+
+      transaction.update(user1Ref, {
+        currentMatches: arrayRemove(matchId),
+      });
+
+      // Finally, delete the match document
+      transaction.delete(matchRef);
+    });
+
+    console.log('Match removed with ID: ', matchId);
+  } catch (error) {
+    console.error('Error removing match:', error);
+  }
+};
+ 
+// Get all user matches
+export const getUserMatches = async (uid) => {
+  try {
+    const { profile: userProfile } = await fetchUserProfile(uid); // Use fetchUserProfile
+    if (!userProfile) {
+      throw new Error('User profile does not exist');
+    }
+
+    const { currentMatches } = userProfile;
+    if (!currentMatches || currentMatches.length === 0) return [];
+
+    // Fetch match documents in a batch using getDocs
+    const matchDocs = await fetchMatchDocuments(currentMatches);
+
+    const profiles = await Promise.all(
+      matchDocs.map((matchData) =>
+        Promise.all(
+          matchData.users
+            .filter((user) => user.uid !== uid)
+            .map(async (user) => {
+              const { profile } = await fetchUserProfile(user.uid);
+              return profile || null;
+            }),
+        ),
+      ),
+    );
+
+    return profiles.flat().filter((profile) => profile !== null);
+  } catch (error) {
+    console.error('Error fetching user matches:', error);
+    return [];
+  }
+};
+ 
+// Resolve match request (accept or decline)
+export const resolveMatchRequest = async (requestedUserUid, requestingUserUid, matchId, accept) => {
+  try {
+    await runTransaction(db, async (transaction) => {
+      const requestedUserRef = doc(db, 'users', requestedUserUid);
+      const requestingUserRef = doc(db, 'users', requestingUserUid);
+      const matchRef = doc(db, 'matches', matchId);
+
+      // Update incoming and outgoing matches
+      transaction.update(requestedUserRef, {
+        incomingMatches: arrayRemove({ requestingUser: requestingUserUid, matchId }),
+      });
+      transaction.update(requestingUserRef, {
+        outgoingMatches: arrayRemove({ requestedUser: requestedUserUid, matchId }),
+      });
+
+      if (accept) {
+        transaction.update(requestedUserRef, { currentMatches: arrayUnion(matchId) });
+        transaction.update(requestingUserRef, { currentMatches: arrayUnion(matchId) });
+      } else {
+        transaction.delete(matchRef);
+      }
+    });
+
+    console.log(`Match request resolved successfully (${accept ? 'accepted' : 'denied'})`);
+  } catch (error) {
+    console.error('Error resolving match request:', error);
+  }
+};
+ 
+// Get matched user UIDs for a specific user
+export const getMatchedUserUids = async (userUid) => {
+  try {
+    const { profile: userProfile } = await fetchUserProfile(userUid); // Use fetchUserProfile
+    if (!userProfile) {
+      throw new Error('User profile does not exist');
+    }
+
+    const { currentMatches } = userProfile;
+    if (!currentMatches || currentMatches.length === 0) return [];
+
+    const matchedUserUids = new Set();
+
+    await Promise.all(
+      currentMatches.map(async (matchId) => {
+        const matchData = await fetchMatchDocuments([matchId]); // Use batch fetch here
+        if (matchData.length > 0) {
+          matchData[0].users.forEach((user) => {
+            if (user.uid !== userUid) {
+              matchedUserUids.add(user.uid);
+            }
+          });
+        }
+      }),
+    );
+
+    return Array.from(matchedUserUids);
+  } catch (error) {
+    console.error('Error fetching matched user UIDs:', error);
+    return [];
+  }
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/firestore/userProfile.js.html b/coverage/src/utils/firestore/userProfile.js.html new file mode 100644 index 0000000..cb2713c --- /dev/null +++ b/coverage/src/utils/firestore/userProfile.js.html @@ -0,0 +1,334 @@ + + + + + + Code coverage report for src/utils/firestore/userProfile.js + + + + + + + + + +
+
+

All files / src/utils/firestore userProfile.js

+
+ +
+ 27.71% + Statements + 23/83 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/4 +
+ + +
+ 27.71% + Lines + 23/83 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +841x +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1x +1x +1x +1x +  +  +  +1x +1x +1x +  +  +  +  +  +  +  +  +1x +1x +1x +1x +1x +1x + 
// User profile operations (get, update, check)
+import { db } from '@utils/firebaseConfig';
+import { doc, setDoc, getDoc, updateDoc } from 'firebase/firestore';
+ 
+// Unified function to fetch and listen to user profile changes by UID
+// (supports both regular and transaction-based fetches)
+export const fetchUserProfile = async (uid, transaction = null) => {
+  try {
+    const userRef = doc(db, 'users', uid);
+
+    // If transaction is provided, use it to fetch the document
+    const userSnapshot = transaction ? await transaction.get(userRef) : await getDoc(userRef);
+
+    if (!userSnapshot.exists()) {
+      console.error(`User profile for ${uid} does not exist`);
+      return { ref: userRef, profile: null }; // Return consistent format with null profile
+    }
+
+    const profile = userSnapshot.data();
+    return { ref: userRef, profile };
+  } catch (error) {
+    console.error('Error fetching user profile:', error);
+    return { ref: null, profile: null };
+  }
+};
+ 
+// Check or create user profile in Firestore (uses fetchUserProfile to streamline code)
+export const createFirstUserProfile = async (user) => {
+  try {
+    const { uid, photoURL, displayName, email, phoneNumber } = user;
+    const defaultProfile = {
+      uid,
+      profilePic: photoURL || '',
+      name: displayName || '',
+      email: email || '',
+      phoneNumber: phoneNumber || '',
+      major: '',
+      year: '',
+      open: true,
+      locationPreference: { inPerson: true, online: true },
+      listOfCourses: [],
+      description: '',
+      incomingMatches: [],
+      outgoingMatches: [],
+      currentMatches: [],
+      pastMatches: [],
+      timePreferences: [],
+    };
+
+    // If the profile does not exist, create it with the default data
+    await setDoc(doc(db, 'users', uid), defaultProfile);
+    console.warn('New user profile created with default data.');
+
+    return true;
+  } catch (error) {
+    console.error('Error checking or creating user profile:', error);
+    return false; // Return false if an error occurs
+  }
+};
+ 
+// Get user profile by UID
+// (simple wrapper around fetchUserProfile for non-transaction use)
+export const getUserProfile = async (uid) => {
+  const fetchedUser = await fetchUserProfile(uid);
+  return fetchedUser ? fetchedUser.profile : null; // Return only the profile data
+};
+ 
+// Update user profile by UID
+export const updateUserProfile = async (uid, updates) => {
+  try {
+    const userDocRef = doc(db, 'users', uid);
+    await updateDoc(userDocRef, updates);
+    console.warn('User profile updated');
+  } catch (error) {
+    console.error('Error updating user profile:', error);
+  }
+};
+// Example usage:
+// await updateUserProfile(user.uid, {
+//   name: "New Name",
+//   email: "newemail@example.com",
+//   major: "Computer Science"
+// });
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/index.html b/coverage/src/utils/index.html new file mode 100644 index 0000000..a828ad0 --- /dev/null +++ b/coverage/src/utils/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for src/utils + + + + + + + + + +
+
+

All files src/utils

+
+ +
+ 53.5% + Statements + 61/114 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/4 +
+ + +
+ 53.5% + Lines + 61/114 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
auth.js +
+
29.09%16/55100%0/00%0/329.09%16/55
firebaseConfig.js +
+
100%26/26100%0/0100%0/0100%26/26
navigateToPage.js +
+
6.66%1/15100%0/00%0/16.66%1/15
theme.js +
+
100%18/18100%0/0100%0/0100%18/18
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/navigateToPage.js.html b/coverage/src/utils/navigateToPage.js.html new file mode 100644 index 0000000..33f9dfb --- /dev/null +++ b/coverage/src/utils/navigateToPage.js.html @@ -0,0 +1,130 @@ + + + + + + Code coverage report for src/utils/navigateToPage.js + + + + + + + + + +
+
+

All files / src/utils navigateToPage.js

+
+ +
+ 6.66% + Statements + 1/15 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 0% + Functions + 0/1 +
+ + +
+ 6.66% + Lines + 1/15 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +161x +  +  +  +  +  +  +  +  +  +  +  +  +  +  + 
export const navigateToPage = (navigate, pageIndex) => {
+  switch (pageIndex) {
+    case 0:
+      navigate('/');
+      break;
+    case 1:
+      navigate('/groups');
+      break;
+    // case 2:
+    //   navigate('/messages');
+    //   break;
+    default:
+      break;
+  }
+};
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/coverage/src/utils/theme.js.html b/coverage/src/utils/theme.js.html new file mode 100644 index 0000000..bce7248 --- /dev/null +++ b/coverage/src/utils/theme.js.html @@ -0,0 +1,139 @@ + + + + + + Code coverage report for src/utils/theme.js + + + + + + + + + +
+
+

All files / src/utils theme.js

+
+ +
+ 100% + Statements + 18/18 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 18/18 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +191x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x +1x + 
import { createTheme } from '@mui/material/styles';
+ 
+export const theme = createTheme({
+  palette: {
+    primary: {
+      light: '#E4E0EE',
+      main: '#4E2A84',
+      dark: '#361d5c',
+      contractText: '#fff',
+    },
+    secondary: {
+      main: '#f44336',
+    },
+  },
+  typography: {
+    fontFamily: 'Poppins, Roboto, sans-serif',
+  },
+});
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/src/App.test.jsx b/src/App.test.jsx index 6820dcc..bc93b79 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -1,19 +1,11 @@ -import {describe, expect, test} from 'vitest'; -import {fireEvent, render, screen} from '@testing-library/react'; -import App from './App'; +import { render, screen } from '@testing-library/react'; +import { describe, it } from 'vitest'; -describe('counter tests', () => { - - test("Counter should be 0 at the start", () => { - render(); - expect(screen.getByText('count is: 0')).toBeDefined(); - }); +import App from './App'; - test("Counter should increment by one when clicked", async () => { +describe('launching', () => { + it('should show the app name', async () => { render(); - const counter = screen.getByRole('button'); - fireEvent.click(counter); - expect(await screen.getByText('count is: 1')).toBeDefined(); + await screen.findByText(/StudyBuddy/); }); - -}); \ No newline at end of file +}); diff --git a/src/components/common/Header.caserio.test.jsx b/src/components/common/Header.caserio.test.jsx new file mode 100644 index 0000000..0b00b7f --- /dev/null +++ b/src/components/common/Header.caserio.test.jsx @@ -0,0 +1,43 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { describe, it, expect, vi } from 'vitest'; + +import Header from './Header'; + +const navigate = vi.fn(); + +// Mock react-router-dom +vi.mock('react-router-dom', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useNavigate: () => navigate, + }; +}); + +// Mock the useAuthNavigation hook +vi.mock('@auth/useAuthNavigation', () => ({ + useAuthNavigation: () => ({ + user: { uid: 'test-uid', displayName: 'Test User', photoURL: 'test-url' }, + handleProfileClick: () => navigate(`/profile/test-uid`), // Directly invoke navigate to simulate handleProfileClick + }), +})); + +describe('Profile button in header', () => { + it('navigates to the profile page when the profile icon is clicked', () => { + navigate.mockClear(); // Clear previous calls to navigate + + render( + +
+ , + ); + + // Simulate clicking on the profile icon + const profileIcon = screen.getByRole('button', { name: /test user/i }); + fireEvent.click(profileIcon); + + // Assert that navigate was called with the user’s profile path + expect(navigate).toHaveBeenCalledWith('/profile/test-uid'); + }); +}); diff --git a/vite.config.js b/vite.config.js index ff0b21c..320240f 100644 --- a/vite.config.js +++ b/vite.config.js @@ -18,5 +18,8 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', + coverage: { + reporter: ['text', 'html'], + }, } });