From 594219a8b0985d9d79071558ab737e0161cbf1cd Mon Sep 17 00:00:00 2001 From: Yatharth Bhargava <52095139+yatharth-b@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:35:04 +0530 Subject: [PATCH] BoG Spring 24 Deployment - Friends Feature (#328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚀 --------- Co-authored-by: Hailey Ho Co-authored-by: Hailey Ho Co-authored-by: Samarth Chandna <57265280+samarth52@users.noreply.github.com> Co-authored-by: Nghi Ho <38119460+nhatnghiho@users.noreply.github.com> Co-authored-by: EmilyAL001 <70612063+EmilyAL001@users.noreply.github.com> Co-authored-by: Sophia Lin <52588981+sophiazlin@users.noreply.github.com> Co-authored-by: Nathan Gong Co-authored-by: Hailey Ho Co-authored-by: Hailey Ho Co-authored-by: Hailey Ho Co-authored-by: Hailey Ho Co-authored-by: Hailey Ho Co-authored-by: Hailey Ho Co-authored-by: Samarth Chandna Co-authored-by: EmilyAL001 Co-authored-by: nhatnghiho Co-authored-by: Brandon Ho <104395400+iphonecats@users.noreply.github.com> Co-authored-by: aeluro1 <103622874+aeluro1@users.noreply.github.com> Co-authored-by: Nathan Papa <111544731+Nathan-Papa@users.noreply.github.com> Co-authored-by: Joanna Cheng <75443336+joannacheng21@users.noreply.github.com> --- .eslintignore | 2 +- .eslintrc.json | 7 +- .github/workflows/ci.yaml | 21 +- .github/workflows/deploy.yaml | 2 +- package.json | 5 +- public/bitsOfGood.png | Bin 2404 -> 0 bytes public/bitsOfGood.svg | 12 + public/compare_panel.png | Bin 0 -> 66425 bytes public/compare_schedule.png | Bin 0 -> 173815 bytes public/donate.png | Bin 0 -> 834 bytes public/exportIcon.svg | 0 public/invitation-succesful.png | Bin 0 -> 4983 bytes public/invitation-succesful.svg | 26 + src/components/AccountDropdown/index.tsx | 20 +- src/components/App/content.tsx | 3 +- src/components/AppDataLoader/index.tsx | 276 +++++- src/components/AppDataLoader/stages.tsx | 259 +++++- src/components/Attribution/index.tsx | 13 + src/components/Attribution/stylesheet.scss | 6 +- src/components/Calendar/index.tsx | 283 ++++-- src/components/CombinationContainer/index.tsx | 143 +-- .../CombinationContainer/stylesheet.scss | 4 + src/components/CompareBlocks/index.tsx | 145 +++ src/components/CompareBlocks/stylesheet.scss | 16 + src/components/ComparisonContainer/index.tsx | 876 ++++++++++++++++++ .../ComparisonContainer/stylesheet.scss | 298 ++++++ .../ComparisonContainerShareBack.tsx | 89 ++ src/components/ComparisonPanel/index.tsx | 176 ++++ .../ComparisonPanel/stylesheet.scss | 234 +++++ src/components/CourseAdd/stylesheet.scss | 2 +- .../CourseContainer/stylesheet.scss | 2 +- src/components/CourseNavMenu/index.tsx | 1 + src/components/DonateBanner/index.tsx | 53 ++ src/components/DonateBanner/stylesheet.scss | 23 + src/components/Event/index.tsx | 34 + src/components/EventBlocks/index.tsx | 38 +- src/components/Feedback/stylesheet.scss | 28 +- src/components/HeaderActionBar/index.tsx | 57 +- .../HeaderActionBar/stylesheet.scss | 8 + src/components/HeaderDisplay/index.tsx | 4 +- src/components/HeaderDisplay/stylesheet.scss | 5 + src/components/InformationModal/index.tsx | 63 +- .../InformationModal/stylesheet.scss | 33 +- .../InvitationAcceptModal.tsx | 328 +++++++ .../InvitationAcceptModal/stylesheet.scss | 55 ++ src/components/InvitationModal/index.tsx | 726 +++++++++++++++ .../InvitationModal/stylesheet.scss | 542 +++++++++++ src/components/InviteBackLink/index.tsx | 158 ++++ src/components/InviteBackLink/stylesheet.scss | 79 ++ src/components/LoginModal/index.tsx | 30 +- src/components/LoginModal/stylesheet.scss | 26 + src/components/Modal/index.tsx | 11 +- src/components/Modal/stylesheet.scss | 6 + src/components/RouterComponent/index.tsx | 21 + src/components/Scheduler/index.tsx | 58 +- src/components/SectionBlocks/index.tsx | 5 +- src/components/Select/index.tsx | 5 +- src/components/ShareIcon/index.tsx | 23 + src/components/ShareIcon/stylesheet.scss | 14 + .../SuccesfulInvitationImage/index.tsx | 144 +++ src/components/TimeBlocks/index.tsx | 21 +- src/components/TimeBlocks/stylesheet.scss | 10 +- src/components/index.ts | 7 + src/contexts/account.ts | 1 + src/contexts/friend.ts | 52 ++ src/contexts/index.ts | 2 + src/contexts/schedule.ts | 26 +- src/data/beans/Oscar.ts | 34 +- src/data/beans/SortingOption.ts | 11 +- src/data/firebase.ts | 11 +- src/data/hooks/useCompareStateFromStorage.ts | 99 ++ src/data/hooks/useEnsureValidTerm.ts | 4 +- src/data/hooks/useExtractFriendInfo.ts | 138 +++ src/data/hooks/useExtractFriendTermData.ts | 110 +++ src/data/hooks/useExtractScheduleVersion.ts | 1 + src/data/hooks/useExtractTermScheduleData.ts | 4 +- src/data/hooks/useFirebaseAuth.ts | 15 + src/data/hooks/useFriendDataProducer.ts | 41 + src/data/hooks/useMigrateScheduleData.test.ts | 3 + .../hooks/useRawFriendDataFromFirebase.ts | 194 ++++ ...wFriendScheduleDataFromFirebaseFunction.ts | 266 ++++++ .../hooks/useRawScheduleDataFromFirebase.ts | 36 +- src/data/hooks/useVersionActions.ts | 51 +- src/data/migrations/2to3.test.ts | 3 + src/data/migrations/2to3.ts | 1 + src/data/migrations/index.test.ts | 5 + src/data/types.ts | 80 +- src/hooks/useDeepCompareEffect.ts | 31 + src/hooks/useRateLimiter.ts | 80 ++ src/index.tsx | 4 +- src/types.ts | 24 + src/utils/misc.tsx | 11 +- yarn.lock | 25 + 93 files changed, 6574 insertions(+), 325 deletions(-) delete mode 100644 public/bitsOfGood.png create mode 100644 public/bitsOfGood.svg create mode 100644 public/compare_panel.png create mode 100644 public/compare_schedule.png create mode 100644 public/donate.png create mode 100644 public/exportIcon.svg create mode 100644 public/invitation-succesful.png create mode 100644 public/invitation-succesful.svg create mode 100644 src/components/CompareBlocks/index.tsx create mode 100644 src/components/CompareBlocks/stylesheet.scss create mode 100644 src/components/ComparisonContainer/index.tsx create mode 100644 src/components/ComparisonContainer/stylesheet.scss create mode 100644 src/components/ComparisonContainerShareBack/ComparisonContainerShareBack.tsx create mode 100644 src/components/ComparisonPanel/index.tsx create mode 100644 src/components/ComparisonPanel/stylesheet.scss create mode 100644 src/components/DonateBanner/index.tsx create mode 100644 src/components/DonateBanner/stylesheet.scss create mode 100644 src/components/InvitationAcceptModal/InvitationAcceptModal.tsx create mode 100644 src/components/InvitationAcceptModal/stylesheet.scss create mode 100644 src/components/InvitationModal/index.tsx create mode 100644 src/components/InvitationModal/stylesheet.scss create mode 100644 src/components/InviteBackLink/index.tsx create mode 100644 src/components/InviteBackLink/stylesheet.scss create mode 100644 src/components/RouterComponent/index.tsx create mode 100644 src/components/ShareIcon/index.tsx create mode 100644 src/components/ShareIcon/stylesheet.scss create mode 100644 src/components/SuccesfulInvitationImage/index.tsx create mode 100644 src/contexts/friend.ts create mode 100644 src/data/hooks/useCompareStateFromStorage.ts create mode 100644 src/data/hooks/useExtractFriendInfo.ts create mode 100644 src/data/hooks/useExtractFriendTermData.ts create mode 100644 src/data/hooks/useFriendDataProducer.ts create mode 100644 src/data/hooks/useRawFriendDataFromFirebase.ts create mode 100644 src/data/hooks/useRawFriendScheduleDataFromFirebaseFunction.ts create mode 100644 src/hooks/useDeepCompareEffect.ts create mode 100644 src/hooks/useRateLimiter.ts diff --git a/.eslintignore b/.eslintignore index d568d086..08643bac 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,4 +8,4 @@ /tests/fixtures/** /tests/performance/** /tmp/** -/src/vendor/** +/src/vendor/** \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 53067ba4..69b301ef 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -61,7 +61,12 @@ } ], "no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }], - "prettier/prettier": "warn", + "prettier/prettier": [ + "warn", + { + "endOfLine": "auto" + } + ], "react/require-default-props": "off", "no-await-in-loop": "off", "camelcase": "off", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index efc92b5e..5302d9f1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,7 +9,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' - name: Install run: yarn install --frozen-lockfile @@ -28,7 +33,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' - name: Install run: yarn install --frozen-lockfile @@ -58,7 +68,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 + + - uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'yarn' - name: Install run: yarn install --frozen-lockfile diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 2f305f28..b0e8a0e0 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: persist-credentials: false # Fetch all history for Sentry to properly create the release diff --git a/package.json b/package.json index 4e0ca5cd..92c73493 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@sentry/react": "^6.12.0", "@sentry/tracing": "^6.12.0", + "@types/lodash": "^4.14.192", "@types/react-map-gl": "^6.1.3", "axios": "^0.21.4", "cheerio": "^1.0.0-rc.3", @@ -23,6 +24,7 @@ "html-entities": "^2.3.3", "immer": "^9.0.6", "js-cookie": "^3.0.1", + "lodash": "^4.17.21", "mapbox-gl": "^2.4.1", "node-sass": "^6.0.1", "normalize.css": "^8.0.1", @@ -32,6 +34,7 @@ "react-map-gl": "5.2.11", "react-overlays": "^5.1.1", "react-resize-panel": "^0.3.5", + "react-router-dom": "^6.17.0", "react-scripts": "5.0.1", "react-tooltip": "^5.5.1", "react-transition-group": "^4.4.2", @@ -84,7 +87,7 @@ "secrets:linux": "echo Enter Bitwarden Password: && read BW_PASSWORD && (bw logout || exit 0) && export BW_SESSION=`bw login product@bitsofgood.org $BW_PASSWORD --raw` && npm run secrets:get", "secrets:windows": "set /p BW_PASSWORD=Enter Bitwarden Password:&& (bw logout || VER>NUL) && npm run secrets:login", "secrets:login": "FOR /F %a IN ('bw login product@bitsofgood.org %BW_PASSWORD% --raw') DO SET BW_SESSION=%a && npm run secrets:get", - "secrets:get": "bw sync && bw get item gt-scheduler/.env.development.local | fx .notes > \".env\"" + "secrets:get": "bw sync && bw get item gt-scheduler/website/.env.development.local | fx .notes > \".env\"" }, "eslintConfig": { "extends": "react-app" diff --git a/public/bitsOfGood.png b/public/bitsOfGood.png deleted file mode 100644 index 3c1115d310cbce138bd6b8fe6b7565d54c65ebec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2404 zcmV-q37htbP)^00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yPD4wK~#7F?Op$M z+cprs?gU~ch&PZjfzt`7OyG0^sT0_pKGG^dP=jPr$1;u}0+Ez*oKGBVB!LU9X`Y8;~y>4Gvc zGR^~PCN`nYvvfwr89>d-Hq?}nkue#5q2X9N@>i7cW}uc;(h|E;w;GXtynFZVfig}P z;>9gov$9B^F!2tzsBbxjbs(1uvMqTZ;22USYLDcg=?4{MB!iN3AKQNZdm4t7Bfk*o zUZh=DsT=_4452z?BhpT!@8bHBG9DphQHYQ4)b@uw(7iB%m05M}g2GUWv{5tm*(kM| ziC4W_1?A;KA4B>=#z+WSGpmvK3k^p>tx}ibb0yYO|I#agXwxk{pPe?MegDuah3X4S zc6%?)eBCnTE6|lRaLQx~J>k1>Rnb|t!VB#6a{`Rw6R`>$?eUt2p91%#hW`=$w zjzK}~n);;IT4HwnDn5P_>35Mnye4ZR5(T^7W!bzeLe|V%N{@ovF0L1cbgziC&u*5C zQ-;@?S(%K52dV7rJM}5)rO(kd^InVcKM6(on?{7P04^0@lD~&jS<^!Aqix2V>6A%a zs>=se-w!$=D7i@YX1D&{$I4_3S)=uEbmc|Ka*Q_f3Tn95zYD{&3!mpH0iank!|n$) z@@;ARqL=53t)c8WTDP51E=aiccF3ZuZw>RlKm~scwz$NzmlnB5_uvez?E2U`%ACV4 zYnkUQYxv1h=6mfUppB^9T6mkmv1azEWC1&B7sD_g$mkyGuju?B~EwK z&v%r$2HVv)$ZC-F0{wtob$lrvCBOV#*L}Xw%C2kEEu-Dyxjcfs6wtqu+a8}+FuyyU z^`jdc^!-!mq#jD>K|9WzY*#j3p~iQ@&1_D>Aq3 z;G%`6|2Tl|O2eaR)ZQ|>A+z0FQ{PPhtx0|rql@x(1lFy4cRFR>GMXBOK89n3KHzwO zT_8@D3r#yX)-alE??S%7w?EZObPmOlHLo=2=nMgGKSuG#e_P?k$} zt+TXNq%W@_N1KNpw2(LlpR0?<9lIW69tNUe*N?lRIzs8L6X|m=eWh@e?0Vy%=lRmy znUlXxf6L9O=|k+b5_ zvc*%zTrQ)^#L$moEx#d((y;43UpiS@c{D65FAU#x^#|d-2QxN?Zzv2l`W?LysP)fB zvYWi6%5U)c5Bckq`9RYnWDtuTY;@&EmFcr)zPIFkL4co`>>Er`CdRozDzsb&i?1K3 zmCKIIFLQ;+-Qp8R3dL}hImEiK@pVee#Ygxk^Tv`V8?=Kv0!#)UxH{uEnivyob~&Mx z`Y(|_iqxIXlIC{9NXr2;^pQ#JIrtW?a#F%z0bWrYhA1W8*hRDuBQc!COu6aj68>S{Z;GXOoOD}bNP8VkPq85TWoh98JQmJ<=yQBapb3RB( z3>!Vs0X-gQ*mbNj8#l8o;51G5#UM*pnC9HUSHtqgRc3{i2hEaNr9P;gS-IxfdpG@2$LP6a*P|bE#Bp%RCmo&QgIg;Fg@bqMFAjRXud$zKpxv?SrGvh30;w2Cpa@TsjIdRfbe=%#6@IS2rAGcIB270GO9+L9+C0^Cgb=d7icT9YsP3Kz8se^50rt zO5Z9?48`k(N^)m$6gL!F_fDbeX~?llu3q?6kGBW)OhY39PocF1J^gOgi@N1F(C#pI z=`p6glOx)V)Bf)BMg`%x%GAkv7vsf9Da#&z1x?ToOicKijY-$Z8E+Gs88P>(Sg5Jr zcT+42Ji`jKrfKD%(QRs#lo1Em*L+!#GrWQ4eswuP%vpZ-ian79dmtZe|M5`Uci%!ubX|Pt48*z{J&pgLzzi;c@2}1k&%&+k&%({w(&oj Wm$Bn + + + + + + + + + + + diff --git a/public/compare_panel.png b/public/compare_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..c8df6275b93c10bef5f823bba92e6be0ebafa5b5 GIT binary patch literal 66425 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 literal 0 HcmV?d00001 diff --git a/public/compare_schedule.png b/public/compare_schedule.png new file mode 100644 index 0000000000000000000000000000000000000000..c1c78ddd41e3f3adf4320d3c6678e99630f5f098 GIT binary patch literal 173815 zcmeFYRZv}B(=NJjhlM*VoZ#;6?(RW?2Y0ss!QI_0xVr=o?gR_&5G+7|v+{ml-hW^0 zs&jLyPSvh`0n}W~HAi=k(cMpXKQmHANg4%_01*HHpvcNdr~v@bSO5T2F+42z6C7tt zOYk2AM;Tog0DwXM&ksb2U}^>ULkJf&X)(Z;DWYTW1+=B8q9_1RABXh&Jq!S{G+I_d zRKpYEEC((y$1UZ4`gYUjr1)Wz5TW2f1qMs>a|F|8KSM)FkBw;~poWBs%CRONB~a$F zdpSe4^kMUmf4co<^Lxhkcib5YqgTIIIy2fIx*ejtrXPjN9>;Xe#}cXPpGL_2P?na> z{YqMUhIW<3;K@RP;9q<53Kc!E-oG;nYWJVko=&Q2Q^$1&l)cf>ARuaOLkl7Pb6Y6T znU4rNt?i27^-M9SKpPqc8w2N<)EF}@9!USsgZ}*D#r$n(Tw76hRhj*MDm{NDGX{XZ z15pqm^54ZmZ>gp4?(%zt)a-Z4vmv|O+bu3Pm#IPSH0ICHQTx&gaT$d=s;H`xrKF@_q5m~ZJPGuGn5%0O0@ANl z>DZG~Ut>v_Jm@fWbWB>xKil-r)4QP>aLCEYDcRWOwBPTP|DvUH)mP!cW`z(F^YY>i zgZ_J8F|HFE4v6VjpX#-!#!+hic2#=dCj8g0U)8j=Y5tXymtRCjMo#W0mHtk-tGdR~ zM|l|z6NSsEPor|96B9-0|5`QM0(dn-Kx}*WG4V^SwjK??yIfju@Wo#1+&61fDk>^9 zJ-u-ZxW9|B|yBaABK|1WYAnxMor`*H_TU$w>x-Rk|E(L1@yn}fkaI2Jei zSKd%xNGmI=_|#M!1Qe8WoB7+>B5Bf%PLIo$ds5frf*Fe&7bF3K|VG%W@!HjHb&gY?`~1;Fyz*QnZ2u#T{BZ`iMXbSyqb53o6WX#V}GAEEg>5vZf+)YR0Q z>oN*jhBG^}|K;mK#0v`xuFYedzXJSgn$f6o>@$**29aU@Zu*|Oh>1ylaY>0=vn~-E z7PUtUOepl{f1Pf$Eac({a8v{keXnXs$jQO+S8Je{kf3W(+yD?B@i@m`!`PVle@uTn z6SEgA3&euWJ5c)f{<-iYsnZZwc*Od$cFe!4hu)HP(9;%sv`EU!NByhP-n_U+6W{~w zzaJE%YHX6FwcTYlPFUku*G#Kc_KU*X%m*VD^o3Hame%uIrbF8wyg0t86$PssHRFcDq=d^p-D zcr-MR062UUol%~5%fIYq@?ryC_pi}J8 z8$W(VCK1pH4h}Zo?DA@GJyu(7u!sWA5vr(wio-?Z2uMygpO)fXP z?KcO-;1USKv$KixkSkCx1AZeU5Wn5OKKH(@zvc{(67x9b+ii3#zHXp9m%bl^ zA}d2Uk*)8$et1Z`nc|;*y@gK50H6Br4R|Kd0;kFdWMDKz7ZJ>dgu8vK4nROaU~@fE zdB)d;qm_iDy>t;_H>LCsn+=N6~AIZ33K?Jdg)4@PFKDWA-Y;123+)K2}W+4 zj+|EnAQ6ZTDCUh5fuL>ZmZHB+PfJ@D;K1Mk>W1<8qr`Slx5Ji@JW39`W$a`Qzji9K z@kdA~J`-`1J}$i0u-Go}N~7(NY*;f9=z^F70|VyY+nhd3XG;yPw>q?{eFFQg-YTga zS$st}hH#k#XasNCzvcV(@}PXeE-{)B8VpG$ z2q0kn_`PkGh(W@Brrc(|#r~e3S^#v1oYxot1{3iv2n+_GOfyPKXc&$Up*$A~8oHhC zHH@bBms`wWJeQgixQQqoN8X4TcW?qxT?^cz*o~oF;ARp|VLCMO+ z>16w7V4U3I^_|^vs%cBRMsNS4jwCq4X49MW2Y*G9!;3eKadJ&JfWGRPw?a1!reFf8 zqwS+9K}p}f-)@r;QzC^f_rEP79jj^Gr1SXh6yEjvbhYf4AZbFU%bn^917;IFX{o>-{#ktB_xRL0nf@7)Zw#$l?n9OuYfJ-jsP&_`r(+>%< z|C%H*Bjfm^3QT;?)z1lL7aW#9+lq4wCnlSBm<+pMZPt-sStMzHLv>*10a8NwEG&rO9UiHJ>vp;1k58FjcG%jWI+;B{+iXbeq)mxBMDH90lq&RzB+LhFzF>{lUtVSJucN@+JM;b35P+)`v2epvIK4S@Q|jUpx{qE#>vU4 zrms&A&ToI02#udFnnpwfV}j(zn?rdyxz9sGLo!ZI^k?$P^fC@UjV z7abb#_Po_#IZcwm<5XziJVwRl|J$?u?e)2&uu!bUZWSCi|GVOKjt~tkGCteT5cJjk zy2FI=+mp7njZLE4>5{C3M5uwsh8vsXwur2vqWz8FtzIA=tJMtX+26vVWbg2BVy)S> z<}G+I6akY`P|)z?&;_L=3Kz&!_sI`L z2HYx$v=99F8f9@XnZb4b9vml@@~;!5NfNY;yd>r1k4C#r@gw#`%K?Da{lVMgjr2Cf6 zvjSI&de1Am6O6sJYuCN7oOLFT5urpY7pmQ6Ly-|#p2k5J&8Pm}NO2##c2IeXHh+GX%T1tXCKob7FJ z>})iG1$3jwTLk`uGDpnMhA2T8l?&_(yd!-C&)NCg?*N`3;6M`q>trQrh)gOm6A@l03D`gq#DK>M<7%)7nA*mEce=P( zjy1a<02WSQymYn{KpMkJZKq8eV7VldtpSP}?sYa=&e}f2b z1~`a-1_2pxKxmeY_XD8_T0KBwV#b7o1ZrxNC&*A(a~5+;%PjaHspzBHKqI74#J*(s zX_9|yYj|GKKOzKPKX1MD82Da)<>JC9L9uGkQBxy_fr0r64gk8=;_|lAislr*_h^I@ zy1E4D{rXz)&f)ua)@*AzgCGR4ZS+s|on2fkZVtY2$zf!mq5gU;Rm?iGw4^L)3^;r- z8;J!Y(*OAX9PX8VgSl*3BV9v7!=Ay{t*tFK%$ZR1U@=L_FnyUTSON}MIJkB9JgkAi z0ZJAYm2qLwLkSsUn)m(o7+lU@G~k92|LmRaLoo-jo}`bo4Dp6%6n_8)@lwFJHP!S>@*;&%cDi=FG%C94yOd`b90P}PibTmg4ldi+9;)AWEI65bLd;XRaqsM;#}V2AM(o8 zvg)4egSW;Twu78-{6PL}+A$O6PLF;r<&g*;EtbmPuvSvqEmA56x zE_;BAs$E7SqKC$KcXDMXphcx@&yYI!)xH<1es6PCmhBy*!a#^JJkGDF=VvFs5<00S zw>=!xnB=Uop$QNsgn_iR+BE#SU}v*LDsgrEMX=c>KaJ`^ne^JFZ)sLZ)m-n|xt&#FB;-E;$}Bso3HZR{P>+7}QnQtpWc?`KKZR!+r8#s68z= z@-IY$wx(iXQV%;s_R|#!irZ zfY>L@X%&O+yTv-KGc(wuj%^@E0Tb6Y*j|JT>}X;?xfefeyM!kY9FO>zC5khs^)fZ;ZE6uVwB|42teReT;ppI4UJ znJ2VjC171+Wt4UTgSOli0g|w>QV|$F>e#t96MEIhb?9=M%#)EtLRRqPXU^)Ie=pEk zEqY!COGG_1{y13JX+Q1s{rH9A`vB5wFY^}7p29yp-(ykgk}$m{*jOAj@_=4)W%na7 z!hOW!lj^?f^fJ{u-IGv;@yJA6k7S*um)|?82Mm3>kG&S{snm$D<>GE{!RGi`MAY+~ zg*OMA&hIKu1hBD$V98l1FvR!Er98DrYLcfFV;(xr_g>#On8+j}z(zFvKG^k*$`?^+ zbNNun$GUTWGZSipDRp(T;E%4=06S$Tr;9`@eN;AEDK7ct zREJGS>1zzY1qYqg+uA4c`NhXlTme4$baBkvR6M~o{1}S-wg*OMoCf;y-l#~_Hs~e< z+)>D|tV0|ct&UP5Of7;O=;gwluxaK_x%<}AH_ZM5`{G;7G{a1cZKmqQ5-=Jm^9DZe zdyh3#IBsaF=mf#P7tIV;p^B2kC|kP38J;)og;F4a`EAG2h+VXox(@QH6z5Jfw z&?^BE1---AoD!>L?L#sIxet?wGW`9U^yP3DaqbhQgdE2LKJCSmK7q0+>^aWv zpd;AArvg!bYWS|s-tXh|0GwClDwnpdX5@mlwL5>uz}{$O6izsds7G#(v{et$lGoUU zrH6-31*9SLz~mLD8tZaLe<2}*;~tu-SS_855g@ic^#c++4Q8y7WuL4*%aGF0fKeDj$It3{IvPT-;`fHt3M6XAb$;PEYjhvDkisNW>Y!+gPNF?v(B6 z5~Vnq>I-O6Ny9&%Y}^9|o(XN#@Anqb44G5kCw)xEvOTQ-tx{4wRn8{bPbj_{ zqIu+QAt++?z)-Q0Ye>=iWikyY_+?dBSR0mf2x29rWB7yDn@D>k0*|p>$XtZ|>f-&2 z8d@jJGuDjFyz!>0jVa`)=C#TEwN1OHc~fzDIg6UTmcL|)>sJo!>-W-54sVr0L)^I3 z@@f+CpK;KCo^c@}?wyH?27w_bd@(P%4S?=6lkvxv|6_nb5#yHsO*(W{qw0EQY9tKr z)4TkP1)!VfMP`B(74*mi0fXO-ic<~|!-E9J5Bp zs8%H{uKpsVW*wU}zk+BQu`5EtRLQy15AkrU^1cQE(dvvxr1e}*1+N5^Bzxe)#v6_s zy zU-}iPa`%d)C0x)TOP6W`BExMtHm?3dJZ-b5g=HiH4l%1*t2hGGP~e8m7Ih_0kBL=s zikbP*gU7+bxK?up?FIn2J{y7l!zZOD{48*_BO1$m?Q+=EO5g( zG@gW<>ASI*US8$u{#`2B3`upp$No}_7aG#8$KpGgD8vTXNZO1x6N%)-d{f8)cQae z1@G7h4oO&#O=*acK2FoT72He~aw?qI%_fK)0@kPo1tfUvKkkA2)aL#G5nMCD`0&v- z*3&w<(+DQyCR}nA8v0iK*EQ7(13xNe@JtjO(t+WVr5`T7*GnPPzF5rt@9XQykh7PGLJmO>nk{Sw3)aJ8Et8n zq9^moMxnOIVq1ak9*+?ku02z7#fuCL9c|6ZA?o|7nYxN%iM)mKu^^ME5-CEeQE`ro z+pQ)`*7+5?5>j@&jISEmQ888X`vRvr6?S}q76UK+wBO7K@abyoQ=tf&gW`kHb0{#y zEpD!D{&a9Zz;itBcpFg^ZY|9Dg@hxw2!DAOPd_rG(;GYr49JoS9YbS>l_+nUvEn+r z+pxH>gK!uzE-!?}S?v62Q~kZLDp6#GDVvS$YGwhr4`&6!uXexs$vVsR4wB2x!>FdK zqiW&IQ0A1ga1t#~6nVAOy)D2sKJtxLQbgUt91SFn#x-f33ctRziPmSeTe`$uUz4$| zgn>+bht&E`q;hJ=}6dU+6S632knCj6_rD`y!4YmBN$$SdSmbW zfo$Nm7fHlfQJGkZl&cLvCP%~GPjj)wes2rgdV$vFp(&VIyh+qvmru4&kVM_f8g_xb zS{mmzeGxrqo@j2xNAWM$JBTqo9OE&VyLaCW6^lT$An!enjaUQ(tZXguDyT3kRXbZ0 zrtJQ;PgCqjb<^*!zcgTE?`jCvQooRpTR2+nF7{_uqvDNocFa2j1H~dPK|X!&)d-Rq0^SG@SUA@($Ch17RL*JLSJ-X$5_VT#j{76nc7K zO1lCSfEZ{+^f&?ZgCTxFeP})^)lrV=0vr|+83UKvdsR6uhENj6wNuKeqBa4uD(DvB ziX`~1z+u*!ogK|DmnK`ZUcW9LE0)ZihVvaUu~?c1>@US$O599~v*4S1MQxy+WJfiQ z>@YZ}=@gGiKYXK`u+g7~9>-wceM`h6TN*-` z5>f3;gyI2kD}Uy;g!aSS{O)49=@!DrjN-vI{PGWY7UkA*fhYyzeb=|$)H{8enS6-^*RUHtuEUNsKzo|Tq)PhEMfAA zrjU9eWiIlDr1~%{G29P^lbRaf>oH$XfQ>aI2bw`MT~uB6bGpyM!d_Wxn%gH6OO}at z%puKIw_8VVvrGr=BpWC!7f=oS4BO;+SjmF_rjl%5&R#1`; zzY*=7umk$PTVwHBsuWvb{Pr`e=Y*cBwX^$%8i=Q|UTTas1@VI7D9HN$i{G|OnJk;# z76dkMTptELf*7y%jD=&0%GK=c5&JH&v<3AU{y_x`z6^s%q`U=sMy2Fa;Z;HMw{u?&j?FLqxAgp z_6czv+B{sp=t>p)cAS0_N{EW-=a5Nr1Rf<+3_wAn+7S{iEk&Cc6o=dpGn}z1A^m_K zN00ATRXxOzRLSELI+sWq+0BWDAABrOhU^pE$knI#MxqOEm5W`1`rXm3Zf+s6_zy6& zj78C1Ojk=_BF>OPnEAbb?%@+2W4oMrJGe#(yEqmDqPib0 zj+PV^YO26<_~33IZX8H9-LD53iW~=V&uK0UBi&!uNBH@B;C5d72|EsNOL2Puc6Vzhz| zv>q-FU6rv2{E%X0au#36^)Y`_+?-iNL3dCHagX6Y=8xyr0QH#?cB82fdcSpj&~<83 zt2{AK%>TGBij;aCu?y`>P?Dk`CA@5#Bj-EqekVhx3kElK)qmJ6m>F4w@M>A_?=Ce~KPfb28ux7_^Np{`q*#ur-RuD28hLi+PDVF%gPT*;_g@3)2) z5lP?F$Am^G@GK2n{DV*hz^x7;VMe3Gg&B&C8%Q?>Ahzf^MK)BiD!FMe(-Ob}&f%Of zP2Wv4PU8jcpiD=iSNElyGA0mo1DN&aDCbi-GF0h1oVI&+|FmjZHq;3d#{G~yzWE#T zxzEXHLzmj)WPoH9=B=IQt2br70ThxSkBd&%Sjs3aBd*-{UOl zY{5z7en5@Za4PBBxJm%GH>ZOoA8u?eXUYKhRxa~~X5sXUAM z!|55;k>`=V5aQAwqQx{__L1;pK+&_)xh5DbzHc0f)4=Pi5(MLl+egxm!2_SnG=J8* zyQ3EhkN=RO{50O}l4zHSVaFhc=tLVGBOxwxSiVY^+S}PH20IeA*7l=zN}jd{hixb+fKa#6o%CKHUlSA0^k!l^cCN+3oLg;7`!KeNyE?Q=0T<3f6=tymU!T99l6imV44@JwR6h(k4=@(vD zuGNEvRYX^fili0J#wup|)Rc;ebv*E_Y>Q+sdV%j11Pr1@B*Ga{6@3!&Chz0*vQe+^ z>a4AvVAb0UOb;AZ1boXvtXkuvruE^`x)@IsX&ST z>{av6Q*9r8v-Ld#R4+*hL|Wjua8W2<^nGY*KCBM%N>03@6<4&V5fBJJ`pYROD3s+r zGwL=Jt6y@^)60*pQV1|8Kte&4bQOa8w%y_b!RwTEdj=J#(JAu~ZizswZS(hBoY@o{ zH77Ptfb2NYnwa$XpSrrs4BZf)saOy(Uf@*L0(H0DVsc0cAw}g}^GjNBw7JJ-6FZn1 zA=L^YIe%g>tgt_^Vre?fDlfF_{cnc=@#--5s8L?g7m`C=I$^dP_SN=h-pLea#Gz-C z7`N-z@tXuQA#;ck&_L1HlX|2MAd6!R5bKIK8JFmym~s$85q~xx?}4(nu>Xmi^Db7F zCeJa`_^oFlZBNEcF~)ZepU2UpEU$47Tx3rHBh)~hpw92Dv`B@&`JKJD`uf88XPr8I zK$Rg@O3x8WmV25UYhYHP%ogAtwZs8N2;3Fw;MdWiu?)e$?w7CZbg*@bIVuR$_7IgC z`qHIGR?NKnKP*Nvp?{JQxc>b!JrVk+u>ap)435*DGV;?l7Xn$M7Y z#%7`B`FVL*n%ry1whp^(te@HFctq65p-NP_`((a;W*5MQJp81zTu@U)C2vbuqpRs5 zT4v|kGps#RH{Q zl$zW+Hm=qmppx9LaBvzB5M zv1jQyAWs@t#pn6EJKN#v_r-G0Zzs4w|DjUO4Vp+Se5Ej@G<*vkRQWQsI1)~8@pV?N z!r^R9G(vOaEcKle8#R@J*y3tm+nC3F%2;I)&2)NhIsvu(@Q7UR5vV^@I{NtO;eu^7 zE8s(i9GW70!YR$r@wGv)*n7>SY5_g62~>?0qO%JYAIDGw4)QfADWS&Z6+tZqDmED* z_IFAcnX)-i9SY{LX)BU?Lzo4oXboG1P#9#K?`WgJ?}xW2=EPeMjyq6#CB6 z7y_FR_usWoA}%t-R5<%eYN<`_W;iNLG{>KHlXX7nT=mWh6z zdZ*@+;tokskwYdH{a8Z(&vk6C3)ZoI?+x6z=mdNyWs=3n`fq=_>xGc##Jt@9?Yi}? zfF7p`HDX$x$372Yw``N75zrZDq?t#(TDGRN;eL&nqu>ehTADHykjW{2KA@}^#GLeph zq|iZY$ObcY)e_f6y%wuH?n`^6+5>*S{GL=YQN4&59j)v1a_%zVA(z|a*I|M~JTX`D z4)uL6Rj5&kqEmUv%k_1htEaKnAU17r;(LFBk-eERp#R6FZX}zZN@(@+sb1+J8!ea4 zLB-2w)t$W^O&E7ZkO(uhshp%?lEwULw<;k|NkH3Ys%tipUR;th=7c9Yn?)g$x8aj! ziK>nDlx25UDoC_sjL~-J|GfKpqiN^7oCjE4-L(ombMcgi)>wzT6e)=X z30_yL71wM+oC+kmIn2*U1}<&5sF8_dFN1p-j8s3@J}yK0Irzu?K76cj9=U~nJcese zy#^fatsyuan7=3$kz|zIrHW+~p%;J$Dhc!>QWF#WawTPf5stH?v{5U+kGhF?wGKL9 zc@kR72VEbJbab;+?GJom(kEaD<)5 zp3SQ(l5~Y#RV~D6$O%xUBTRyM!nKSn*Ly!yXB#A7Tq7`f-F*x5wJZImKNXi0;*tLO41EM77gQz-oi{|Pc7 z(6&R}az!}hP($dESpoQ->Y-6eeUKdYDt!7OD#I8N6FA zcbOCB8_lMc2!b7g9{W^HH%8~iYp_C8QOfU9Wuex4MS_jB-2~!mq_16Kqz0I4+wo{I z9->91s*V*<)8=1bCS<{O$DTZ4dO+m8*o(I>=1<`{I%m?b5jYB;D6`_f&*pP$Rg|BJ zP!0t3B&p8jE7?R6(eaTf`C2O#-udyU=+J0aY@_32fu3)3Un~rRKJ)LK0EUmcQJQ+Q zaPK-#*`^if7{LwG=O%d9_X6`-0&<(6xq&Y*(Al1~diF56m%kb-7TaP}e9GjF^fmaS z-ro{>yEtV0-VXNH;yQvy+_a+rzl5+poNKBhR`=3QS2V|%y#01LSvC9BBgq06goKWC zt=H)B?E|;t$5(G<0yp%xhZxC*6PXkS0urye*ANX3QIq^%SdEt^Ntc-!CKeGu6O-l5 zndcU+qH({TQFw(IZQ)q1D@;3DBnBgSw6BmD3o(6X} zT!flIFPro-d7~>zSM=rjE+oS0H3ig4ISPa!@tE;~hjKGtg)_}l?(#!mx^CkdXr(>t z@+hksA-kb_CJBAU&-TZ5Pm#`*4Z^v9Xj1mO4MKESNhI?MXOy@#uvSUM;w;OG!wC|A zW^1|Ks z3hL)bYKl!QAI*0+V(!41YJ=Il=}}0+iNU{dV(nfpup0^KXHszl$fqa#(S*@5LUHq$O1j=YN<&gFtdY=rMvqsU?+lmH5t742c6zTgZsQ|akB@0dcq>4(VIm9CU zPIDhy^OV{sz2e=GaG2Z`EsCcwBb?3%&pwDj$nG*|n1xRtj+zVJ`s05A-6RZD19+U( zpTqSZA)%2Yklf02=m0b*~vzx!De=Geg!Gq9_7d%3+@LzSu&bt72}+x#WKNF6kRrkM^>t4?4po<-G?j{Y;m z&sGQ%^2l3Nvzs1w$W61cemKJ1E*ocS=!s2p`<{$0h-fmMtstlz|M;m~IQLsnM-DSM ziUq1Tz0S}!i=zKbmAWKj5@E@!M!LNc5q`ibQ>UtP`#HE@WM;S3=3zZ6N*7$F_h}eu z{WCv9r#Lkw8~aSCmq)IA(`qUCKpeBu{0))|5+WdYa^l3Nt?}G7tyLkiRSnsEXJ-XG zcG{;&yG`0bAj85MeJA=K=U{PT!9j^?ik+cF5lMy>j#EUfmw~{kCRP*|c@h4dxx$GS^{C2iBQAP4&M_+G%QDXJ1 z72L3!;&K?zoLb|p9{T9QKdUqn*3k5~93R)KqDc3_Ud!Wr|~#y#; z2G}tr=NT+)@r9;&BIIuOP&Dq$sm?B8&1V?tdKXXcfl1M(&Iqu*16g&9wVM_s`LYVtbI(Yu#HFcljFQkcIHy@Zc039Sn;G6W++vQl>MP7oX4!}oTkZRFZn<71BoVro7u5~2BF5aQY_C798!|%I%fzD zXJ_cJ3@8focq!*A{5j$(*Xw9nv|-T}52YE$O067}vLH+%hGa63kZ3Ar&4N43n&dw2Xbn9AY)6$71I`4qjNGP<9U^(Z3jg#8|NghtAl0{orPg(Uk z6%-H>7*uI%gE5_@t$s;Rg+DKP3%v|6a#Dys`*VB>gI8;5`{ZkW?2?~L61Y~7nPxW( zgGx>%%h{YaI-jaduqSw*$4NWKNSkjw5cU);HQ(oV^iq^{Xl+j94!vqU5h_O zTj#M-#oRJWm=&k3k7MZ5K_HDx_;j=eX&Z=#OJX%5;u`QeBhze)lBx-ahX~3Ql$N_S zGIy;_U~a%4tO*xO0#jyald4r`3BLvLLt5Y(tFC`y`23(5C=y8`5yoFSf<$H+qR#vZ zt|;eVXr%$nx+Bh}h^V)Nrg2TqsmPpP#4IZ%`sGTTyq}xM)#{7nwxRm_41en$(rQ zE@W-Y<0++~+t00i>H@yYue{K3;f&2eVN<7zWFsK#HN<8UgWqKWb0eVVu(e4l>SDj; zEr)TJ^6|u!u&5Kx#uJmE#2Kv^G0?l!M>jx(mk`+N1= zrq2Vm%ew>W#Cim1WQF)SHw&>Ghf<^CnytpGI>(D?L=gbvv?^MGl24gSs(P4e1N#lC ztORbAE!0{fA@n~tdk_hi+m3Y}2nE2DrxZ9G%B;p;_qlBAXlyJO>FCVh)FPE^Biv(E+jRw0Y}3U+a&OOTC2d@SFSu|p+-x;gXS&t<*F&B`N@w6$@=XaT#E%E zL*Isrcnwx{%GE%y**&^pnx3!J-6>T-dNT?%&>2q`sCMYz1=^q9@`3aOdJ0CNlZPTi z+Wp0{SN)T&*m*GKQ?~`Q917p@z}P7=_yD=&lo-(*L=0dxpegOb-QXg z)L6$;JR?;y?WRLSr;9ato38q$wk0}ZyD(jdn^`W2ygdaQS=Z`tEkV8Cpo0Cu7~pttvOZkv6#1vK?Fy+x>H;6 zW+Ba8j3qT!+!^$(vWOsql+k+zyrHP6;rHGYObjz@=xfW}rf*=*9sfZ*DZPE&^QUX` z97FWsZSteCH#w8p{97$9s z^p-~j!Z-6RHj?ICt+3CD$+{JKv;4^!RbPskSOyL+VUHTQIaZW^X#A4;fcJ!&j|B*D zZ=*qVKUMC7-c(ouY52`-QOD@NJX5?Z1UzP!GH2` z1D)}tM6(Lhy{YZokJiou?WX&?`ftDe<_WBRuVnYR1u!CusOlBfG>nRD8W~sm#$?iD zF3XLpxdHB2!N{FottBy+A>-U`N_a#d>GTzy%{JcF^XI>bySL9zj7r*@>1XYK`_$jk z-0HCeYAk%Xcz5hm=kqVeTFmz!cIl7oeI)r+ZT z?7}1n6kiWYq{m0X>FlakGNAnpBxC}Q-d4Z=ya;4Xc_|7x^M4p3hGBVL_)%F6dc`iFPoviL{J+gKZ?0s&Fp6yI4ol+Yg>_51*N?YdA z;Wf8g=@g$$)F$P4S#I_W_)EgTt?(Rv`pCojdV*FW1+#Xfpm0N5{evU22mU|pqc0?c z^wz4PKlg^oYl~`_h_NY(62-w(U?gw1+r^CSlX}($N2_FB>=~tOuVpata_=Q%Uu~|V z?~}S$hS5XNz?u;{)^ORUF-z*Nr`LqjZTZ0BsYOjQQ*&Rva^JV}Go{wA)t9mrJI-JR ztL*Gs4B&e$xLG$^S(Gy89E+p?enS*D#58zc_<(Axw*_lr*o-Fb>8se!tizJgD1JGM zW})3F3-IVE9?YiHF36W8iUE+1-+#B!_0BhFkW*gCl$Vl{b@Pj|6G9agouWOy?swk! z#iN^~x=J4$Jkc`@uA!sMjYaLt)~PE2F3Xe9_!y*xL0G~lLL3uIyPQAIT1>GEXp0}H z{!=ruiBL?-CWv-K+`sZ7pXINBqRz5UZ|5ah3zaX=>C>`5OGO#n&Yv2yF=Notz@1qjxYy18)Z${jgcR*om zy#tikcn1cVgrj(Xmya>NB#hW7uegR)AS2H8*@Ki!elPOkbdmVf3*zZe1fbF3@{&1C zT=EVB5A<|~#-{4`6bS#g@mT=-+o_%Y3XE{?rdPTHm`!ZVFr8FHkY1cGzK0+NBb;KOkL%B`-?2Mfs!iC`~BH&}0C<$l(8B@2{WY zincIJ7cqkm9yc!LvAlf}H?mwN`^`H_`!eg=*NFCm zB$`ACVe~j{xt%0W&QC95L+X1v%?iTYB*^cO!Q@)fzN5XdO~rv;&R9#Ehfeb>D~TrI zDJcDVE~}c<$5A@=?cju9-#<|)@oT+&KG+!g9%BE1p_y#4KL{4d28$4TQn{b|oW9(7 z16ST|W4jNUQAtT>gJHcA#SlF(UUN-eo|~_^GeR6Z<{fpw*IJ~2RHk!pspZljGylv+ z#jOJL$9k^dMTUPtE&g2m>_eYgc2}=f(THl|l(pvwVw`Pls8JpEMxMK05#(fK)Dv6I zD;?yR{K2=Jh(F-1gL{;8L=z>65~rmy=i_c!b7IZ@JHRMlukkw~PUa71wUQ3_ zX-L{s^;p4%lHqkxFzn&GWJI8HEpOGgw~FWSZ$k)*iDiR>qy4j=A2&=|tTQt4p!;M$ zc7M6YeZYC_<|bmcjJ>2aum&Wvocri8iDME&l0OuDxtS+znBz`ci-Ywqb|Rh+^IDbG zmz9?Xm)gE7Wz1Q<8GVggOP`j?z0)n#CCH#%P1tWtYPMt@Dry#ZUkN6U`qVN(B=?C0 zhDqTUGdGK5W-|_V@xpEYpa>Q&VG$}q(=#^SHYR7|;YJ_5yU$lkxyj3(z4qF=*^HjN zG^}mpesr!ryQ4+*2+-Ac{lRX87AHW7ddS)?p6v(vwy~%~HSlB=$oPY;)Sz+ zHuYjcmqMt4rL!U=PEl5g>~Il@Y+FU)IQ=_f&mwHCS$lQKW%0Y7RoR)eqgIYw#4 z{2seZQteIZ9Cltd=6JYYa4>9?8f2+N2 z*$}oMAKHKt0#w2dD@@vdR9+{{Zt%Uk$n|PRk7PZ&+@v#P2&SJ!c$qy%4sbQEvwY5UXRGEM~ zn|uc@(HEEM;Cs+*G(OnyqXzxCbp2fd;f4I>g$eY#0tO6JBy+%)#yPKF7Y3|_^7EKW zg}a=k+u2p!O~2My&4dp5lf_+i`x`qtfnH;f(PLWzC{{Ppi{*x#bZ+wTl=Y9LKRkB{ zCHXLDUZUa5zk3O%5%$CW3x3#e=OhU-9}jW$Y%5CAF)wg_$5P}5EepzW4kk@ z9;uV%>s!3rJ-)$~Gly%OUY>m3U-&(qvcF>hZJ*v8lGkJz*O$1UYNnv_*#zKU~G z#;-6%H?En2;Lw&s3>FbF%n*bIos_7sMyeE07RJ{5MBWGN*S@{)9a&xF%(IjDd&lnY zRD~^-w6mkq{uRQtjxp192U`1u#hX*)%%!0hmEj+oT?LhVAfRAAR{8e@(B%pJemHIg z&`lC0VW%4^1P6;Vf3|+Ob(mPO9itAsDLHRcZ|s*9zvm)x%5i@>B={`)Pqh8Br-rQb ziCXrNG#yA5(p71h8%UxmiRi}m5Tqa8LlX` ztvwh`i=?9$7Mjh|lZizFnOI9xw?aRC`aLW5BP|QL7LJcrA$Mnhh%){U{wtTZ(l0sr z(nuMkh|NQV* z(cR%OvXavl37PFVqndEeD9bKfaR(SobcZl0y$HJ~fB*-A2`>4G!V3$E%jW+qLaeK^ zY2(nw-zMHG8oyU0b=g=wbg92@`ZEftmWBuBLc;Z4gZC5w(sE!uz1_@)mSjs8j_t03fS6?({96 zntA#fQ1Qv+zO|`qpLz+Sk-gR`D!MZ^T>V4>$+@g;<+)oTWR&{ASN9A%^ox+BIu4Ty zX+v%J5tZAXa2v5Hyymv4B%;rCF&n>mm2W?`xmWRxF^{yM|K~7iP`xRksn?JaT8Bjh z5gPO9d;KRR+;_`44Rf2d_8Ru6uKLC$i(^HfW`Q#4v(%)5h*SN0s3-0yzSYkH^Mv>a zNTI97mN3M~rw3C)Sa6_Q2^=sm1pKbAVA*l_vp~U3Roke_&Z@vPkHXc(EkAN@7)eP+ zP(XWquK`USA=%@$ofN0Lr!O$#!x}mZ$Yrnb6v9g;^A}%?F0w=f2-U(~o=@mIEIAnP zrD#@A&Zyy{gw>vJJyjP?S`yWTN=~*t)6LSB)ggQZTpAL{4#p;%`LaiW^FO$xR~#%D z41;FiR;rkW42|4D%ifo#UN zG5yIcXIYtTpyG`DD~gFrb|$8+3ywc4K0DeBEGy0<39$w5090zwkhI)lArwD{I3sxi z#J*N>m#s3dYYP%}O!-w^WDnx^To<(FdGXhd`bH_SM7ZNF=r;H^1V0lU@rH$(SyIi@ zQPPgag}cNC+POZjm+>LzvvrY0=970^GwKln4{1hoKd(NY11XP?ECoQ&o3)vca{_wy zkqUsi-G4f<+WwzAQSw&w^p{z_NCx8`hIWBo{8T4(K5!o8? z;hX3zVu=zp`twq5#OX}hnKAW)Mw+R} zOSPVt#OjE<^(a6T+l2)eB_h$5NruDE%vro}ZjRUnThJXUQDqZ0w({xE*JlkWxInBT zKTH56qaOpf4NdVNPVv$}tU6EoY;yxx9sqn>OK;Z%FmEQ z94GmrIx!7_u%WZp@>nUa#Y*Y^CnCoD^*$~OoE*pgEfwS6&7Djxd)~+uF*+!i@bDP1 zltg7W#ghjFu$5~ypV7Lr$P;!nQ3I{5r|BX@zP+PQT@v}r=QFVhK2~KC|8Tynt!V_!vN6~7bki$v&uZeMVbUF9k zZO>y?1~IT)I5#0G`af*GuFODlp=D*(#@V=+*9(i6#X`WN=4WP@s_XcvjC56o{7|w* z!{Kq^%rbO?AY}MGEnZ4LBcjOT|6zn3kD5?b8y5zdOMoWoWBE$16&b&P;+w5Tr3qa< zbyclYZ-B}J!cjZZhY7s?2-H;7WJ6w&e8WY~v!Qo}vFT^GBQ%^=mtSDGrPtdN$!GyA zlG0fF{omk-Mn%A1bE8VCPCtH0{H|(_6Kqa{h5KH`uM|(;o4B0 zR>T@b*fi&sQ{Krlvd(tT>W$f8eVitu~)mJc;3>A zYln;?U8xPr1{rzrb*CVgb*K=pD&NQA`;RVXwbRoqOzcZ(+U|9Qou5hXz^UQX|EhZ= zezX&M*q~16bQVmJVGSs6EG6nKm|UtER!ayt0dg*(yZ!9qLogUpoCKBNO9f=~T+NL5 zGjZONq~B`l4jrqIg~>OKfU+)s9z(zmB*J54ge-2FsXp6_p;Z|7BLlkCDF2SNuAozE z0Yajpy-`tboXVXo%e2am-(82sR)|8JElQ*atY}$5Q;B7QH{2-%e?fGFklVBKb>%A~ zI|t=Vo49v<^L!c|nmmdP#cGZqY%QUJ10+1kgy#E!e#8&5h8vSLW}cWB(V>gjKbA^C zGJ*fB3k2*uJ4+}GQ?)AozZFfMr~!RjfQ+BNv|6&TZ%B{mA>kr|a%@B1PVBMU9C5LD!x^57qSO zL*L^x0w|0FmxIvJ)truB?8{lOiS^Gfc-G7~MhT{JKpp-#QK7X|;m0iUiJfHW&>SRG zJNk;5CRlG?qH$|6U+^I=#9NjP7`Dlpfp0p}@WI2(378(Qr$PctIx(!<=KOa>9H)wm zM1qWQ;7tr-{GYwNt?4V@<@Uv?#zw5Jz9wNeibLN29N;F%C1wwW14<|5qMDpMxMV*c z{4=3p$bIZN@OAP5pKlqzxR_*hgv^8*9kJz-;S7pkH!^iv&MFEUmD0W@?2S3bpO}I9 zV|l_ypeDa3QvO=SUh~BYE(%eLiKsakvWU{${g(B%SgZ_*w&M`B=;LNnn>f!lsyC0* zOeDjV1IVu838)7~0N*u(Lx2Yd9rvt-!B?Kp&t&7buh7`F#-8OSWcm)UwIY@2AhRVX zyX_oSZGMhH3sn-$RNEyI$pA51#M!!UPSZkkpSm~F7#rcy*a(HeU9qL^>r;})JJl4w zmRZE6HVkTb;^}8|8TA1wW&t|1u*X0gU5lwzd7}9zA}A>G!-a!7JXr)m9#3*ZRI-+w zQjH??(AX5$r!@8tMMKqk7eG{f8j}f}e;K)2I~29r0zdR{Oqj z7)i(l;)zTjGE33n4yWnh&~Bj|T=zIUcwNs<^VmHL3jd<>{fIHu~?e1}{0nfYH|03&k9{aZfN8@_+D zb8R@%j3M$kG_3QJw|)9_gt-6$HF7F~9CZ0pk*OnGHH{ct05r}PL2apZFZ5pr`C}3r zFI-Sk58+9Y#BvcnImMQOK?gT|+m+=iKk@{er7D0gS;aYq%G~DxvN`x8KcGt^AP^0zB<83I<$6GYgy6d4;(72=cY)5jkTF3AM70pVu8ZL_ zpH9J>iB2y2T!)dke-cd2FQ}6pK@RMdyR3b`T5g8gg%gGvxG*#1MQ(;(GqlKR2MBqz zgBBz0U4VsP{L*WTjI_x@8f!ap$3)XD2k7@!2L+gCVEM%*$yw3wk(N*o$oxp5#Ho$^ zd?Lc>5M+F7m&M(r2izx3qlxTy`X3UCNDMw4xNdOSP%uvyUuCC9Sn)hfrO-x&oWhBG zfIfC~uKE53K^Tk~YoRwU{DCCf3-vI}!0#%daLG(n!8hT0fdGE2taaGi#xiVy1*uJq z4ALblEAOk?rblslkkPziE1hY$(Z<{@Bh_LC;?(E5mWBwN>%!mh^l+~#i{to*J5&Y7f>9xfmr=Oo;8pAR-Y z6+1yWxZiYFyteQ*L^+7hY$aS~Y6^!TzH^D1xLyd_q5Oh}v!DJi6Gfuba$^1ct#z#1 z51RKi0}l||ULiudG@hV{mBbzC{Sn57wqT1}jzRk)Z?U^+6^3ZX6HFaAoM)i;mo4{0 z0mXjbnQF!PJ<x=(DX}ZH94K_2d( zPjgopEsw1a)87Pef@vh!-?cL3>)xzHC^29LRL)C;r(yz}ih}*0yj;ERII0IC&#v@W zlWD%uGVNLb>EZ@*tM{?1iCW-A$71^Zoa&Ok7^ou}E!)86-dc{Nb(bXldi6N@pwxgJy$UP<4G# zna=ivDS;Jr&@~9=AswO*Lk&<0cnNT__niC|z9;%(BI@m(a~aXe8k5_l=wYp=mLU*eO*k{Oe1i#mN&R|QT+FU%#`Ep#0g=AtMg0sigZ=+ih z0Pkl`Uk;bvWI`;~e>Zka@1xIw-#?KgK{P~->*Bs#(1L`!UH0&A&2KP;e0MPLF!dW? zlWt|LYe68gCtxD+y41N1u@NzIzEu}p3nzEdHc%ww#HruK_)Gk)F?X0~YsgujZ!Zc< zX9+*G&{~;0ZVKYEFt4&@Y+{9`#J4YaVlguly|ItKvQqGI9gc!gZKIzp>~lxguw?fe zlylbuvAFxzdg3gblo#(18~LL1%hfWK0Ti&5PN-H8qhzt8=pKI*VF{&S3>+``&ditP ze+uM_I_nJ!U97@AYo}>_b@htx&7a4x$?4H$tnn?oFNT-eZ$;!is8WAQpb9E&LLc1$_jb1x_ zY@VaU>n4G8%~f07_c_auVDi{yojJt1sE2}R>KAO#&z~Rh_-&+uvCRKq8(Vt=e|a-=f$28;_9eH80?|-=dxZ zJxS?Bte)HUL*Gp^Zgglo+Zr5u-hti)-~}2fyXatLpKt+a3k&Sj!OgHh8y-0XJlb6U zSt=SO(UX5LdR6mLXtIf)D z{aCV3Qp`KC@C*n73`fe0_5KY%L(Q?xnThcj^KVj7F1hLo)=#3$VjvhEn2Cre5@vD0 z4M<49zpC-*pJ45K!jbq95cVl=aSCOxL|vVAI$%{3>7O^6r~w?=TU&R}Y6fCrM(8=Y zZ7z*M)T^FcEEmjz;@1{)<4BZg>zdHu4lhgF)D!w*EWI9k*ZsM}+qsd4Sv#*GN*bZf z!InuPLfY3GZ42ki7!L>n<#n5&5@O0Q3`kQmJHc-x6+DOQOdl}|pHUTi z1~-m7_~8-KDJjqFn6%1@JsZ-vH(muX@3q zyHeNZx}-h#D!sMCr^wj5o00gC*jP}@S?lp`7-#MtGevh-`;Pp=RV8{)bh!` z5JfdWnNR%c?B~7z8zz;VMkF&?oPBoO!HLwkpLLPqlRu zEgmqlCDU4L3b%`6cXoVM9V`q?6NOdOkLRJ6tn0srB&>cE)$aAH+W<6cyKin%EIiz^ zgr&hnrvJ>4tAc_WhJD(T5?}_v;Rh&#L=!5I?+LwR=XboX`PQgzX6wzl7y)F!8466s z7`u_t9x^K?B`x0+A*c3^&w;aEft>uaxLTbGL<=y-G5L}cX}IkB`7tf)I%jb+;fygb zB0f#rv3Gex3v565D{zD|KW{J1{>$H&Ts^2-Fj`4=P)*5s?D!lXr>La5VLWLQ+W2F_=<>|&^nt6z zUZgwtFbFPgNs~4;m2|JWVQ>+nV)V zFS8wOQz9e2rBf2Euf*X46l~A()dEN)ZB{Gy%;g3KY@*NNSQ-QxbXPk;GGGJ*PWkA% zQ%6Nd2XYA6D3JwfN+Y4G395`T^=3M&ff#+PgP?e3%s4)o zkCj)P^>t6KLyMJDxfmKm-cx8PUu}`lylHbD&M`|Fn)yJv~91%mW0V6fh3s;r@!AJXWVAT_B!G4^{`ZXgQm7oT}+~_o%Dc*89{V z!@hT~pA;U~I_G4{n*_kDf))GNMt5&@MX-F-)y%lQs|n3coMgcx;+1=TGt&z-z7mZf zFcEOLt~jB@e!$l6AS2dQB|RsiX^ISGcb^1!MHCpqRuW(Yvh|-iE*5d&tzC~?Y8<95 zStHQSaqkD#?+%KL&0e_J zT58qtUhMNe(;M%_Nn_vYoD17Yg!Ox3A8)Ly6*Vq7XI8&o&+7K>KkWZCq>ljWg=@`8 zbT@3<2GY}PO94DZDw&_8oEx5XVTf%mr}6)IsM17(Q3TYSYfcIO!1K2Ydd$yvrwJz~ zR#neJvT@9Yq7p054dMv2+8_}=Vd;GV9Tk#4=Dw_k$dnM0TN1{ww7Ke`M!(5%)|8)v z#+Y<&$XVWB8nGIFIv*i)H%}6R3<{wnq>mVgYU&dE52}Tvl&3dskIWMriqy9;8Wc0@ zwW9_pw4j8*c7ZT_qoA7VWtmrWqTDva)^PNaqg?|JWAdCEDB|IrW2Ci0_&zL=Q~YV= z+@c1q57Q-__sL^(Mn>$ISTo`Zi>Z1$I9R^)$~l_i#lF79$P<$-O_I+i2kSh$#p=Ee zp=W%#iiS|sxOq@YQubC{!FG#)Vetoi#JzvGJ9p`%%zi2gZyiO`Ao*<%}8< z+R^M_L_+9P^hv?@b_VU{sXJUeJlf-hs+1I6cPTo0`cp2){fVUDV7R3g$6xJ_f75GS z&vki;ftVju@&k3D_XXIJdBGmY9VA7GutZNbA$McvTu%dc|5bMSp7p-|;HV?>o+S}4 zTEfx=m!zX@{#9}I`C`@pSe6X5$uuQ-@_-kHXR`b?PfITJ5I%A~bD5xc-%HwD%(7^Z zCy0yTKxcC!%Ga|d9L_@p!WWB6A)jqWDSy^)tf{#3PqBThxRU1VBaiNKBQ>-;n^3=G z6beW%rZgmSyHNzu#j(9z@&1%Ltkv37%CYIJLR8FaD5_@}dLIpq`bYzVviBbFyKBP1 zXZ%?wju8hy-%#p{B7|=}LEo*@o`hG$ufcrrxzvKSRC;elHnpojN;Qw)USIx7(IKhj#KO`$t<-)`Irjc-%PG{t)Mf(rOZ;4s1}fP`6>r za+?m_)dHJc`={?;vl-m%xa>B4!w@0-g}l5vZsmm%2Dkb`RwmUqV~BXa5qn+;9|FM# z)_UK*nF_{`2(H7x-RLj2yVcRU93CFBSk5zG0bJBRled@0vmI1C7Vyl>j10K>ATMI| z+=zriJTlhS=Xpg5V7CJDE_8FGP_Y^_M`I_;LGqjN4ilRYNEed9uG)2MyzXmLSKTPU zL1PqQG9o707LB`gJySD@^Y z#mr!fAUxJ@^I2Z*IuoyjTA8IRjQ!CHUpu@fuO#LHWdH*y@XEVJni1-UKhANH4uLC0 zgIb8o6lKU(1PDjwy%AP*xfbXDaHD|yc|>pFaYV~Q-exyqpPz~r9PG5tiz*v_o|RMp zB@SFC3@niO15d0@A$dpQZjSP)CcTduBci^Ug8er$NBC{Hs?L?iPArIge%5{D9VHs8 zOI;h~us(@C_ETly|%h$ReqHC8bb= zQhj+S&6n#%1q14?7MGz`Om*E8PvLhONKv{Udqjxj2D%!XkBEjSJ#0S(6?^9L5MwQ~ ze-rt%AO7Xbd@;r}J_45c;>~UmIlYw7Aaox1*USvgMjsT(c!pgJVaVZ69uYg~&xgGU zich?RR0TiWpBVBJdacUyp(}l=kr>(CWyR)X*E*ZlATjVJhd-ogiNnCEFCk_ur!=OF z+N$kpphwiu=FWVj=mVshSi<$oSstd8>VLH0XV)cGn%BhXF`z>J}v(m6^P9=tO1|Tt^Q1X`i69$yxcEXCJR1XdLdKXKHnpYy!Q11 zFL&?KE66b;BzD|u%{`+gb8WYSr&k;!{2H7mJAH1%HVq;i!ixGxF)bPprKWa zFX|bq+0D~J-!PJ#slkvpdZD-Gvj}a7WU%BS-p2)nqC|_llO8KN{`_)u)pPIW3Qhm4 z4DIc8VDGM4A#3XhfNI#V{vq(k3;-*huh63YmkMA)=ef_jQB&?G8iF?Bwx3Du`1f}W z18gWX=mRMWDN-m^R2Oh*FxEjZhoS*FEGfuL&E09-dGAnE5?m(e5UD6J{C7Xw0rLEv zhl4|{=i7C5lb7JjMc);W-mv_Pfq_BABV_e2H3p!V>i}|FEH2Sd^}dU)ifPQn{p(=y zWit;xi}x*t*P(Xff4!DDbrA?5#@$R-%vrmXWkY9$jItWc8DzaxMZ*r*r;-3axv63^ zn3OUIpr}xHQ)~hbEfdfzc%T8mvU+|?Skt_bsKJOE6WW+PP5MPLTmGjD5Fh8~W4822 zn&f2R6PbvV(l_87UmQ&i121F2g&j*5b~92oG@yA01(Q6k!47y*;Et7|eo6uggaR?x z5gk@6S+ITOpMP3wif@RuSCT>8Wf8L)=@t7g{eyMf{mR1Cju;pXJ; zJ$jyoADXsV(9GYYc|1w{;UOT(FgQ!bx3Bp8esSc9MW#>$SF?GE3Y&}pZx5yHLkic| zC|vAwvCa0{F`u=E-5@)k0I>uQn(Yw#ZLnkaVma6{`ssodY$5~MYUot%d49UnsAH;; zxnzNWa_}5L+8uxS1rx{9Lg~vWHh`I<;w-h+O4pzTrU(^Xy(|s)DKXK>O4vMi#nwaQ zZ_*7B-Y(g6r|aN`GiaBW=*pmXzi>Kil+R48WaJ9oE*d`RjhBtfie^?Ufj@uGz$Np=CLB2WL!~haJnkkq3=z3X z8XD%U0zpVr__Oo#%jEAss8DiXAPg2-hH2P?)xlIQi^VL>;X+mRt-g>3%qXsJ!pJ6~ zJ40e}mYqyQc=)QIXrvtt1K)J_r_WROD^cg054mK&I2waN+q}PcG;tKMoPs-zRU&2P z2h59kD2Huc7Lv(bp~1eb3I#rS$gDX1>L`6~-FXC6R0LbI`f`5&VpsVWilcWU5oqhm zTi$2w1f*DQ2Su(Dm3Uu3jtMfsCZCFW8i?-YfFrL`GtZ?3f6lVmZ5W zo*up$jYQ5MYAvH0&uwBiU)^10&F3ru^*vsaKFIF!>5PR|k@}|U7~yr3*ZTGi$fPlCGLigKAlu=8F`y*;tl~%)7Z|6hiK?o$;GY77)0-^v|Kp&k+ut z%UhZs;20pupMbsv&4zl;N^ydu5p_-A`h5VueR5i)C~c+aQKnFSr^u&PA1AL>OA@LN8UYFd-S1Ou@_9)c1b zG>M<}^?rV=G5?X+Oo#SjWe|PtKPOSf@X^lG+g;9GfgGZQaeB56#!iR4n9|U zhD^UKMVe7B_Yn_*D<3f!vU`;;`B?p^zEaD*sI|CXm7QWp4qwZ9B*=)Rf5or9Y~Ljy4F+nRXn>qof0F}^x#t65KKLbIX)Msj?MKCOXK@qf{0XH7lKgD_-p=1 zfFMm<7PEt0sDB$DO|-Q1O?_%VkyBmT*tzT~#>*fQi-n-tPRtmBz32cf${ghTCn!#C z*g@!zFxd6N&E+vn$uE4_^s%a`&iWlK^o=y_(gmD)%UNs-hCg)w+Tw&yl?S;yDfqj= z9A4?QZZ?&E%ByNV6n3JHwXf^AloPR|&h7=?CAY%b2Q z$-HiG#~e@BbI!0QK2+`jqzO7{mE{)l@egr?Y=fmnqJzYcjhw#!f)C(JUVm*@hSp={F^dzJCeOMyqh`QPUPi480ViZrnN#6F zBeSy+3@0x5AKZ?=I{d_bcwFkM*$fRiET-=B8^(AjFP~|X>Fbw5>w54klAI-(jUc#_ zIl(pFyPRbW@lB1G%X*%jK1(7qG9NrWjgkLnl1$?ghh?7AW0Ja5D}+d-<<)(dwTm35O- zAEpV0;hb1V3GJeWIr_^e+R&bZh!M~z*)y}DfxhJJYIyYNcHH*FdD|FA6F*ul(TOgu z^PB5exy;d0ixM>tHJ$5`Nr%Yr=(C$^Af6%;k5wFK-75ESvl{%E#@ zqtgCzdw`=91B+ye0CKx`rtqr?5U$n8PsN?{LdMRn%G9!piG`s+?pK2H^968X{@-6D zjr8p@#wW3~lrH_`v)7Z;y%`8y;9(OwePT0FMF06;y3r`{=V{11s76s2=rl4zwom_i zK1gI4i2H%PkpQwk#D%?qoWFnhJoJ`AseM4qjLBFsqtcdQc34r^$5#XcG9Z-)2otlC z?gP$qr_Vocd?yJZw>$3F>t6F241qMBv)6L%mKiER-+YOf)(ycQ7dd}x%wC?H1@89)m zz)8jW7Gex1?c<%`TzV;BsAvV0P%m%37!1^ zn+GsRK-v~ZJ=?6LqiPu2Gv_~ZqIpRfAjv<6_E^{^$>XTf1aP4LrBd)(GmG-wE}V`2Sj}ZPtHt;Qy`9|Mwse z^#57|pvELCK;@L_QZ+?OryQ3Z2Wn0t-PM;!tMOF^V14(FalO@qwhTB zRmnlRwvhQhc$Z}i;txz=7$cb5BtX+*os8}i(ERC;7XjyelK3TEtyc}G;^76mI+bbu z^Xu)K=lj&t!ZrW?hW~5JPqJMr$S-~zF|q4?dM^c4-Nb;V;eWS{%l|Kw72*H62LHlR zK@Q^PHs6zfZ=;OR@-sz^N5$j+k%yx$_-ZF)_iLl9m_9`m7QV2gL@}5;D)yjS{qp)6 zOV8ZgBG!m96|jSm2kiWz6d><87SPnxJXz(T=oS=ud6k>bPESh*z@-F&YlH8*))#KN zZk5I#h^DEOzQ!`r7UwAF91crj8l(h&5Kdx?F zYTk53`&#>tU&EbPn5}eY;i%!3y$X<`M%}V}_Kv-YAzIveTlT?iY%W=H_V)7@Cii|2 z6nyTi5`g$`7AwM`K{bS~k8#C|(J7V#sgGXzB<9@Gly%yx-^Obg4Sq11* zBHQxD?VCt8$kE@PJ(P5{&YXRO>AYXAfIxoQ4|s057C%llG=@i2J8Ynfh@2v}z=u3rF=ti2J}D|&Si(v_6y?zI_?xG>Dg z^fbw_L9crG7N#0A8uFYh-GD@gb{)c)sAalV>0)90t5+l$4#N9o&}U@2w~yK@*z#j4 zpEOQPXa9+nD-4o|JJx?`UEMuywhJp)v&%z;)wSL0$f!uF@YJ(78$m}md^bH+yMu>K zqe(}J;6U&)hhC)L ztL*2xh{$M7=RF*6HiwfY3EsG=s8Vqwrezo2LE$Tt)-Nf`p>Qo;6Kj}D78EN1Ck^Z& z-%9yk8xjv|wtIINSs{_1SFZ@&Z11(G%vS~OZNiYkZ1^{*N=hs0vkew5^gQ@ZNm&^6 ztJW9a!WkcU^J*)V-Wtx%ul`0aoZSE5Ptk99bi?MZeY!wenS71t`i>XAMoGq^_;M?_ zxa1%1>2dmd-GF>7QTPwgamM_&Q5L}Wh}@rB5+`hzk|QdTZ9UO)e*fgv8W6T@)Jy93j4PD;8tU768=LzyB8CWjie}v1ueHB z|G0>~G^vGD*8A z$&4ya#@DQw|8t49C8}VL?Ua$I=5mV~s@!B&O>mrzuT+>ffO?e4Hu}LHlK>?cbjmEy{We3R)#~mtV@udD{oU#~O!@Chku%RYR4^ zyw_)yoH9Zm7i`;CbN$7nRZ7ElCvDv|1(us+I81C}#VD9#FDvj|=A7r~q&a`aRvXWt zI;;<0%P-p+8pl8HFEn-wE?mWyVG=?a2o?XyA>Tf(#iOUdk$qEQQ%ONVQ zJincB?nM#3C+jS&?7i~aj_sRz^+2KMeCgZ(-$I`y*){wDnKTluSS=Bnuoqrco;l8R z)2`7Gd@5-AADxn5MUVXpq&DAQmKLKk+%@FCUrKh2)vpj9pB(SMe<%|9cJe+2m|PLv zuv;3(a~_0~=K;2jSyw(Bt4cl(Z;W*vf?Sd~JrCvw5JsAyVJ`RA?1kZIT5nRJkIo1N&m3K>6L8YPJok%V}5xxjhN~xL_QuH)GN9Btbq3S z+%@G^r;xmFIPuFJ>j`=-VlZV?V%ifv5Y$}f4YkgKSRE_z;l1}uN->Rnjf`r|n!$1o zo@w@|R^2DUQT=n3L^K41#2Pvgnc3^($dg(ls&ArG!Cw6h^}pI1W#)-BtT|8(VfwET zp{Z8P!rZ+HSUJ=M^9w!Jp`WT` zG_$(WUVAIH#w7JLaMx7K3wCX9B`0T7>U8OGJu#a!=eHWmT3u7xQ2*82Z+BQ_SP$I~ zyNDioI!wA6-=KK6cUCj-*zApEZ1)n&f&6wDU^EBrlz8iT0S6QG`UaH_~G%2UL09ipYhXXDvR5uica^h;yRq^!^`m! zi?#i@t^%j^X94

    a>1W8ekiavdJlhw&_~WrGQZXOhN0MRAzq~d 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 v<$hAiG$hC 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 literal 0 HcmV?d00001 diff --git a/public/donate.png b/public/donate.png new file mode 100644 index 0000000000000000000000000000000000000000..8acddde32cb95e6a81c94ebf46ba698a185d31c7 GIT binary patch literal 834 zcmV-I1HJr-P)@~0drDELIAGL9O(c600d`2O+f$vv5yP`ope-U1ITuOI?+>HVXo?u&>s{+E2|7gRnDSz|zA{U1ioPL}zOJy!Y>a~9U4nO^ z-mqn?j=J{>u_Kebp!>!xW5?o7<4~6hGP;qm549GVwU= zg|+NM9gHWmyH|V}Gj&3^Ffy}ElJ@00#gQ2yAk?`}SzO4WxB6@IT`5ZKQ_5UrEL;9H zV!_0WI%Ehy7s`s9w;fGdWv3>!|? zBKO}{85F)${evTuRD4z^YlbW44_0iSN@h;oQgO0oxIo<;&)5yVA9z+3OueibRiTa_ zT_EFu(gJH*^ZsB(_bz1cRCSaD4Z-@sf4VoJj}m;e9( M07*qoM6N<$f}@Idn*aa+ literal 0 HcmV?d00001 diff --git a/public/exportIcon.svg b/public/exportIcon.svg new file mode 100644 index 00000000..e69de29b diff --git a/public/invitation-succesful.png b/public/invitation-succesful.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c0da0ead78e21307f3a5b4d86e3be8202f3f97 GIT binary patch literal 4983 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 BV-o-X literal 0 HcmV?d00001 diff --git a/public/invitation-succesful.svg b/public/invitation-succesful.svg new file mode 100644 index 00000000..354ba55d --- /dev/null +++ b/public/invitation-succesful.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/AccountDropdown/index.tsx b/src/components/AccountDropdown/index.tsx index 6ec7d0eb..4f683bfe 100644 --- a/src/components/AccountDropdown/index.tsx +++ b/src/components/AccountDropdown/index.tsx @@ -1,13 +1,15 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useContext } from 'react'; import { faCaretDown, faSignOutAlt, faSignInAlt, faUserCircle, + faAdjust, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { AccountContextValue, SignedIn } from '../../contexts/account'; +import { ThemeContext } from '../../contexts'; import LoginModal from '../LoginModal'; import { DropdownMenu, DropdownMenuAction } from '../Select'; import Spinner from '../Spinner'; @@ -40,6 +42,12 @@ export default function AccountDropdown({ const [loginOpen, setLoginOpen] = useState(false); const hideLogin = useCallback(() => setLoginOpen(false), []); + const [theme, setTheme] = useContext(ThemeContext); + const handleThemeChange = useCallback(() => { + const newTheme = theme === 'light' ? 'dark' : 'light'; + setTheme(newTheme); + }, [theme, setTheme]); + if (!isAuthEnabled) return null; let items: DropdownMenuAction[]; @@ -63,6 +71,11 @@ export default function AccountDropdown({ onClick: (): void => state.signOut(), id: 'sign-out-dropdown', }, + { + label: 'Theme', + icon: faAdjust, + onClick: handleThemeChange, + }, ]; circleContent = ; disabled = false; @@ -77,6 +90,11 @@ export default function AccountDropdown({ }, id: 'sign-in-button-dropdown', }, + { + label: 'Theme', + icon: faAdjust, + onClick: handleThemeChange, + }, ]; circleContent = ( + } {currentTabIndex === 1 && } {currentTabIndex === 2 && } - {/* Fake calendar used to capture screenshots */}

    diff --git a/src/components/AppDataLoader/index.tsx b/src/components/AppDataLoader/index.tsx index 883546e5..632eebb5 100644 --- a/src/components/AppDataLoader/index.tsx +++ b/src/components/AppDataLoader/index.tsx @@ -2,9 +2,11 @@ import produce, { Immutable, Draft, original, castDraft } from 'immer'; import React, { useCallback, useMemo } from 'react'; import { - ScheduleContextValue, TermsContext, ScheduleContext, + ScheduleContextValue, + FriendContext, + FriendContextValue, } from '../../contexts'; import { AccountContext, AccountContextValue } from '../../contexts/account'; import { Oscar } from '../../data/beans'; @@ -15,6 +17,10 @@ import { TermScheduleData, ScheduleVersion, ScheduleData, + FriendTermData, + FriendInfo, + FriendScheduleData, + FriendShareData, } from '../../data/types'; import { lexicographicCompare } from '../../utils/misc'; import { @@ -22,6 +28,7 @@ import { StageLoadTerms, StageEnsureValidTerm, StageLoadAccount, + StageLoadRawFriendData, StageLoadRawScheduleDataHybrid, StageMigrateScheduleData, StageCreateScheduleDataProducer, @@ -29,7 +36,12 @@ import { StageLoadOscarData, StageExtractScheduleVersion, StageSkeletonProps, + StageCreateFriendDataProducer, + StageExtractFriendTermData, + StageLoadRawFriendScheduleDataFromFirebaseFunction, + StageExtractFriendInfo, } from './stages'; +import { softError, ErrorWithFields } from '../../log'; import { Term } from '../../types'; export type DataLoaderProps = { @@ -100,48 +112,70 @@ export default function DataLoader({ termScheduleData, updateTermScheduleData, }): React.ReactElement => ( - - {({ oscar }): React.ReactElement => ( - ( + - {({ - currentVersion, - scheduleVersion, - updateScheduleVersion, - }): React.ReactElement => ( - ( + - {children} - + {({ + currentVersion, + scheduleVersion, + updateScheduleVersion, + }): React.ReactElement => ( + + {children} + + )} + )} - + )} - + )} )} @@ -204,6 +238,87 @@ function GroupLoadScheduleData({ ); } +type GroupLoadFriendScheduleDataProps = { + skeletonProps?: StageSkeletonProps; + accountState: AccountContextValue; + currentTerm: string; + children: (props: { + friendScheduleData: Immutable; + updateFriendTermData: ( + applyDraft: ( + draft: Draft + ) => void | Immutable + ) => void; + updateFriendInfo: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; + }) => React.ReactNode; +}; + +function GroupLoadFriendScheduleData({ + skeletonProps, + accountState, + currentTerm, + children, +}: GroupLoadFriendScheduleDataProps): React.ReactElement { + return ( + + {({ rawFriendData, setFriendData }): React.ReactElement => ( + + {({ updateFriendData }): React.ReactElement => ( + + {({ + termFriendData, + updateFriendTermData, + }): React.ReactElement => ( + + {({ rawFriendScheduleData }): React.ReactElement => ( + + {({ + friendScheduleData, + updateFriendInfo, + }): React.ReactElement => ( + <> + {children({ + friendScheduleData, + updateFriendTermData, + updateFriendInfo, + })} + + )} + + )} + + )} + + )} + + )} + + ); +} + type ContextProviderProps = { terms: Term[]; currentTerm: string; @@ -224,6 +339,15 @@ type ContextProviderProps = { ) => void | Immutable ) => void; accountState: AccountContextValue; + friendScheduleData: Immutable; + updateFriendTermData: ( + applyDraft: ( + draft: Draft + ) => void | Immutable + ) => void; + updateFriendInfo: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; children: React.ReactNode; }; @@ -245,6 +369,9 @@ function ContextProvider({ termScheduleData, updateTermScheduleData, accountState, + friendScheduleData, + updateFriendTermData, + updateFriendInfo, children, }: ContextProviderProps): React.ReactElement { // Create a `updateSchedule` function @@ -286,13 +413,62 @@ function ContextProvider({ return versions; }, [termScheduleData.versions]); + const allFriends = useMemo< + Record> + >(() => { + const f = {} as Record>; + Object.entries(termScheduleData.versions).forEach( + ([versionId, { friends }]) => { + f[versionId] = friends; + } + ); + return f; + }, [termScheduleData.versions]); + // Get all version-related actions - const { addNewVersion, deleteVersion, renameVersion, cloneVersion } = - useVersionActions({ - updateTermScheduleData, - setVersion, - currentVersion, - }); + const { + addNewVersion, + deleteVersion, + renameVersion, + cloneVersion, + deleteFriendRecord, + } = useVersionActions({ + updateTermScheduleData, + setVersion, + currentVersion, + }); + + // Create a rename friend function. + const renameFriend = useCallback( + (id: string, newName: string): void => { + updateFriendInfo((draft) => { + const existingDraft = draft[id]; + if (existingDraft === undefined) { + softError( + new ErrorWithFields({ + message: + "renameFriend called with current friend id that doesn't exist; ignoring", + fields: { + allFriendNames: Object.entries(draft).map( + ([friendId, { name }]) => ({ + id: friendId, + name, + }) + ), + id, + friendCount: Object.keys(draft).length, + newName, + }, + }) + ); + return; + } + + existingDraft.name = newName; + }); + }, + [updateFriendInfo] + ); // Memoize the context values so that they are stable const scheduleContextValue = useMemo( @@ -302,7 +478,10 @@ function ContextProvider({ oscar, currentVersion, allVersionNames, + allFriends, + currentFriends: scheduleVersion.friends ?? {}, ...castDraft(scheduleVersion.schedule), + versions: termScheduleData.versions, }, { setTerm, @@ -311,6 +490,7 @@ function ContextProvider({ setCurrentVersion: setVersion, addNewVersion, deleteVersion, + deleteFriendRecord, renameVersion, cloneVersion, }, @@ -320,23 +500,43 @@ function ContextProvider({ oscar, currentVersion, allVersionNames, + allFriends, + scheduleVersion.friends, scheduleVersion.schedule, setTerm, patchSchedule, updateSchedule, setVersion, addNewVersion, + deleteFriendRecord, deleteVersion, renameVersion, cloneVersion, + termScheduleData.versions, ] ); + const friendContextValue = useMemo( + () => [ + { + friends: friendScheduleData, + }, + { + renameFriend, + updateFriendTermData, + updateFriendInfo, + }, + ], + [friendScheduleData, renameFriend, updateFriendTermData, updateFriendInfo] + ); + return ( - {children} + + {children} + diff --git a/src/components/AppDataLoader/stages.tsx b/src/components/AppDataLoader/stages.tsx index a371d749..425b5a0f 100644 --- a/src/components/AppDataLoader/stages.tsx +++ b/src/components/AppDataLoader/stages.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Immutable, Draft, castDraft } from 'immer'; +import React, { useMemo } from 'react'; +import { Immutable, Draft, castDraft, castImmutable } from 'immer'; import { Oscar } from '../../data/beans'; import useDownloadOscarData from '../../data/hooks/useDownloadOscarData'; @@ -9,9 +9,16 @@ import LoadingDisplay from '../LoadingDisplay'; import { SkeletonContent, AppSkeleton, AppSkeletonProps } from '../App/content'; import { AnyScheduleData, + defaultFriendData, + FriendData, + FriendTermData, + FriendIds, + FriendInfo, ScheduleData, ScheduleVersion, TermScheduleData, + RawFriendScheduleData, + FriendScheduleData, } from '../../data/types'; import useRawScheduleDataFromStorage from '../../data/hooks/useRawScheduleDataFromStorage'; import useExtractSchedule from '../../data/hooks/useExtractScheduleVersion'; @@ -23,6 +30,11 @@ import useUIStateFromStorage from '../../data/hooks/useUIStateFromStorage'; import { AccountContextValue, SignedIn } from '../../contexts/account'; import useFirebaseAuth from '../../data/hooks/useFirebaseAuth'; import useRawScheduleDataFromFirebase from '../../data/hooks/useRawScheduleDataFromFirebase'; +import useRawFriendDataFromFirebase from '../../data/hooks/useRawFriendDataFromFirebase'; +import useFriendDataProducer from '../../data/hooks/useFriendDataProducer'; +import useExtractFriendTermData from '../../data/hooks/useExtractFriendTermData'; +import useRawFriendScheduleDataFromFirebaseFunction from '../../data/hooks/useRawFriendScheduleDataFromFirebaseFunction'; +import useExtractFriendInfo from '../../data/hooks/useExtractFriendInfo'; // Each of the components in this file is a "stage" -- // a component that takes in a render function for its `children` prop @@ -256,7 +268,6 @@ export function StageLoadRawScheduleDataFromFirebase({ children, }: StageLoadRawScheduleDataFromFirebaseProps): React.ReactElement { const loadingState = useRawScheduleDataFromFirebase(accountState); - if (loadingState.type !== 'loaded') { return ( @@ -521,3 +532,245 @@ export function StageExtractScheduleVersion({ return <>{children({ ...loadingState.result })}; } + +export type StageLoadRawFriendDataProps = { + skeletonProps?: StageSkeletonProps; + accountState: AccountContextValue; + currentTerm: string; + children: (props: { + rawFriendData: Immutable; + setFriendData: ( + next: ((current: FriendData | null) => FriendData | null) | FriendData + ) => void; + }) => React.ReactNode; +}; + +export function StageLoadRawFriendData({ + skeletonProps, + accountState, + currentTerm, + children, +}: StageLoadRawFriendDataProps): React.ReactElement { + const friendDataSignedOut = useMemo(() => { + const friendData = castDraft({ ...defaultFriendData }); + friendData.terms[currentTerm] = { accessibleSchedules: {} }; + return castImmutable(friendData); + }, [currentTerm]); + + if (accountState.type === 'signedOut') { + return ( + <> + {children({ + rawFriendData: friendDataSignedOut, + setFriendData: () => { + /* empty */ + }, + })} + + ); + } + + return StageLoadRawFriendDataFromFirebase({ + skeletonProps, + accountState, + children, + }); +} + +export type StageLoadRawFriendDataFromFirebaseProps = { + skeletonProps?: StageSkeletonProps; + accountState: SignedIn; + children: (props: { + rawFriendData: Immutable; + setFriendData: ( + next: ((current: FriendData | null) => FriendData | null) | FriendData + ) => void; + }) => React.ReactNode; +}; + +export function StageLoadRawFriendDataFromFirebase({ + skeletonProps, + accountState, + children, +}: StageLoadRawFriendDataFromFirebaseProps): React.ReactElement { + const loadingState = useRawFriendDataFromFirebase(accountState); + + if (loadingState.type !== 'loaded') { + return ( + + + + + + ); + } + + return <>{children({ ...loadingState.result })}; +} + +export type StageCreateFriendDataProducerProps = { + setFriendData: ( + next: ((current: FriendData | null) => FriendData | null) | FriendData + ) => void; + children: (props: { + updateFriendData: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; + }) => React.ReactNode; +}; + +export function StageCreateFriendDataProducer({ + setFriendData, + children, +}: StageCreateFriendDataProducerProps): React.ReactElement { + const { updateFriendData } = useFriendDataProducer({ setFriendData }); + return <>{children({ updateFriendData })}; +} + +export type StageExtractFriendTermDataProps = { + skeletonProps?: StageSkeletonProps; + accountState: AccountContextValue; + currentTerm: string; + rawFriendData: Immutable; + updateFriendData: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; + children: (props: { + termFriendData: Immutable; + updateFriendTermData: ( + applyDraft: ( + draft: Draft + ) => void | Immutable + ) => void; + }) => React.ReactNode; +}; + +export function StageExtractFriendTermData({ + skeletonProps, + accountState, + currentTerm, + rawFriendData, + updateFriendData, + children, +}: StageExtractFriendTermDataProps): React.ReactElement { + const loadingState = useExtractFriendTermData({ + currentTerm, + rawFriendData, + updateFriendData, + }); + + if (loadingState.type !== 'loaded') { + return ( + + + + + + ); + } + + return <>{children({ ...loadingState.result })}; +} + +export type StageLoadRawFriendScheduleDataFromFirebaseFunctionProps = { + skeletonProps?: StageSkeletonProps; + accountState: AccountContextValue; + currentTerm: string; + termFriendData: Immutable; + children: (props: { + rawFriendScheduleData: RawFriendScheduleData; + }) => React.ReactNode; +}; + +export function StageLoadRawFriendScheduleDataFromFirebaseFunction({ + skeletonProps, + accountState, + currentTerm, + termFriendData, + children, +}: // eslint-disable-next-line max-len +StageLoadRawFriendScheduleDataFromFirebaseFunctionProps): React.ReactElement { + const loadingState = useRawFriendScheduleDataFromFirebaseFunction({ + currentTerm, + termFriendData, + }); + + if (loadingState.type !== 'loaded') { + return ( + + + + + + ); + } + + return ( + <> + {children({ + rawFriendScheduleData: { ...loadingState.result.friendScheduleData }, + })} + + ); +} + +export type StageExtractFriendInfo = { + skeletonProps?: StageSkeletonProps; + accountState: AccountContextValue; + rawFriendScheduleData: RawFriendScheduleData; + friendInfo: Immutable; + updateFriendData: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; + children: (props: { + friendScheduleData: Immutable; + updateFriendInfo: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; + }) => React.ReactNode; +}; + +export function StageExtractFriendInfo({ + skeletonProps, + accountState, + rawFriendScheduleData, + friendInfo, + updateFriendData, + children, +}: StageExtractFriendInfo): React.ReactElement { + const loadingState = useExtractFriendInfo({ + rawFriendScheduleData, + friendInfo, + updateFriendData, + }); + + if (loadingState.type !== 'loaded') { + return ( + + + + + + ); + } + + return ( + <> + {children({ + ...loadingState.result, + })} + + ); +} diff --git a/src/components/Attribution/index.tsx b/src/components/Attribution/index.tsx index 2f36a8e2..59cb0eb3 100644 --- a/src/components/Attribution/index.tsx +++ b/src/components/Attribution/index.tsx @@ -1,5 +1,8 @@ import React from 'react'; +import { faGithub } from '@fortawesome/free-brands-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button } from '..'; import { classes } from '../../utils/misc'; import { DESKTOP_BREAKPOINT } from '../../constants'; import useScreenWidth from '../../hooks/useScreenWidth'; @@ -10,6 +13,15 @@ export default function Attribution(): React.ReactElement { const mobile = !useScreenWidth(DESKTOP_BREAKPOINT); return (
    + {!mobile ? ( + + ) : ( + ); } diff --git a/src/components/Attribution/stylesheet.scss b/src/components/Attribution/stylesheet.scss index c031a548..efb83f5d 100644 --- a/src/components/Attribution/stylesheet.scss +++ b/src/components/Attribution/stylesheet.scss @@ -7,7 +7,7 @@ box-sizing: border-box; display: flex; align-items: center; - justify-content: center; + justify-content: space-between; text-align: center; color: inherit; border-top: 1px solid $color-border; @@ -20,4 +20,8 @@ flex-wrap: wrap; justify-content: center; } + + .githubText { + margin-left: 5px; + } } \ No newline at end of file diff --git a/src/components/Calendar/index.tsx b/src/components/Calendar/index.tsx index adae1a80..0d8fecd7 100644 --- a/src/components/Calendar/index.tsx +++ b/src/components/Calendar/index.tsx @@ -1,14 +1,16 @@ import React, { useContext } from 'react'; +import { Immutable } from 'immer'; +import { FriendScheduleData } from '../../data/types'; import { Section } from '../../data/beans'; import { CLOSE, DAYS, OPEN } from '../../constants'; import { classes, timeToShortString } from '../../utils/misc'; -import { SectionBlocks, EventBlocks } from '..'; -import { ScheduleContext } from '../../contexts'; +import { SectionBlocks, EventBlocks, CompareBlocks } from '..'; +import { ScheduleContext, FriendContext } from '../../contexts'; import { makeSizeInfoKey } from '../TimeBlocks'; import { EventBlockPosition } from '../EventBlocks'; import { SectionBlockPosition } from '../SectionBlocks'; -import { Period } from '../../types'; +import { Period, Event } from '../../types'; import useMedia from '../../hooks/useMedia'; import './stylesheet.scss'; @@ -18,38 +20,57 @@ export type CalendarProps = { overlayCrns: string[]; preview?: boolean; capture?: boolean; + compare?: boolean; + pinnedFriendSchedules?: string[]; + pinSelf?: boolean; + overlayFriendSchedules?: string[]; isAutosized?: boolean; }; // Object for storing Event object and Meeting object in the same array. -type CommmonMeetingObject = { +type CommonMeetingObject = { id: string; days: string[]; period: Period; event: boolean; }; +type FriendCrnData = { + friend: string; + scheduleId: string; + scheduleName: string; + crn: string; +}; + +type FriendEventData = { + friend: string; + scheduleId: string; + scheduleName: string; + id: string; + event: Event; +}; + export default function Calendar({ className, overlayCrns, preview = false, capture = false, + compare = false, + pinnedFriendSchedules = [], + pinSelf = true, + overlayFriendSchedules = [], isAutosized = false, }: CalendarProps): React.ReactElement { - const [{ pinnedCrns, oscar, events }] = useContext(ScheduleContext); + const [{ pinnedCrns, oscar, events, currentVersion, versions }] = + useContext(ScheduleContext); - // Contains the rowIndex's and rowSize's passed into each crn's TimeBlocks - // e.g. crnSizeInfo[crn][day]["period.start-period.end"].rowIndex - const crnSizeInfo: Record< - string, - Record> - > = {}; + const [{ friends }] = useContext(FriendContext); - // Contains the rowIndex's and rowSize's passed into each custom event's - // TimeBlocks, consistent with the rowIndex's and rowSize's of crns - const eventSizeInfo: Record< + // Contains the rowIndex's and rowSize's passed into each crn's TimeBlocks + // e.g. meetingSizeInfo[crn/id][day]["period.start-period.end"].rowIndex + const meetingSizeInfo: Record< string, - Record> + Record> > = {}; const daysRef = React.useRef(null); @@ -88,11 +109,14 @@ export default function Calendar({ }); }; - const crns = Array.from(new Set([...pinnedCrns, ...(overlayCrns || [])])); + const crns = + pinSelf && !compare + ? Array.from(new Set([...pinnedCrns, ...(overlayCrns || [])])) + : []; // Find section using crn and convert the meetings into // an array of CommonMeetingObject - const crnMeetings: (CommmonMeetingObject | null)[] = crns + const crnMeetings: (CommonMeetingObject | null)[] = crns .flatMap((crn) => { const section = oscar.findSection(crn); if (section == null) return null; @@ -104,27 +128,28 @@ export default function Calendar({ days: meeting.days, period: meeting.period, event: false, - } as CommmonMeetingObject; + } as CommonMeetingObject; }); return temp; }) .filter((m) => m != null); - const meetings: CommmonMeetingObject[] = - crnMeetings as CommmonMeetingObject[]; - - // Add events to meetings array - meetings.push( - ...events.map((event) => { - return { - id: event.id, - days: event.days, - period: event.period, - event: true, - } as CommmonMeetingObject; - }) - ); + const meetings: CommonMeetingObject[] = crnMeetings as CommonMeetingObject[]; + + if (!compare || pinSelf) { + // Add events to meetings array + meetings.push( + ...events.map((event) => { + return { + id: event.id, + days: event.days, + period: event.period, + event: true, + } as CommonMeetingObject; + }) + ); + } // Sort meetings by meeting length meetings.sort( @@ -132,6 +157,83 @@ export default function Calendar({ a.period.end - a.period.start - (b.period.end - b.period.start) ?? 0 ); + const userSchedules: { data: FriendCrnData; overlay: boolean }[] = []; + const userEvents: { data: FriendEventData; overlay: boolean }[] = []; + if (compare) { + /* + Create a dummy friend schedule data object for self schedules for + conforming types to iterate over all schedules in one go + */ + const selfFriend: Immutable = { + self: { + name: 'Me', + email: '', + versions, + }, + }; + const allUsers = { ...friends, ...selfFriend }; + + Object.values(allUsers).forEach((friend) => + Object.entries(friend.versions) + .filter( + (schedule) => + pinnedFriendSchedules.includes(schedule[0]) || + overlayFriendSchedules.includes(schedule[0]) + ) + .forEach((schedule) => { + const friendMeetings: CommonMeetingObject[] = []; + schedule[1].schedule.pinnedCrns.forEach((crn) => { + userSchedules.push({ + data: { + friend: friend.name, + scheduleName: schedule[1].name, + scheduleId: schedule[0], + crn, + } as FriendCrnData, + overlay: !pinnedFriendSchedules.includes(schedule[0]), + }); + + const section = oscar.findSection(crn); + if (section == null) return; + section.meetings + .filter((m) => m.period) + .forEach((meeting) => { + friendMeetings.push({ + id: `${schedule[0]}-${crn}`, + days: meeting.days, + period: meeting.period, + event: false, + } as CommonMeetingObject); + }); + }); + schedule[1].schedule.events.forEach((event) => { + userEvents.push({ + data: { + friend: friend.name, + scheduleName: schedule[1].name, + scheduleId: schedule[0], + id: event.id, + event, + } as FriendEventData, + overlay: !pinnedFriendSchedules.includes(schedule[0]), + }); + friendMeetings.push({ + id: `${schedule[0]}-${event.id}`, + days: event.days, + period: event.period, + event: true, + } as CommonMeetingObject); + }); + friendMeetings.sort( + (a, b) => + a.period.end - a.period.start - (b.period.end - b.period.start) ?? + 0 + ); + meetings.push(...friendMeetings); + }) + ); + } + // Populates crnSizeInfo and eventSizeInfo by iteratively finding the // next time block's rowSize and rowIndex (1 more than // greatest of already processed connected blocks), updating @@ -141,21 +243,13 @@ export default function Calendar({ if (period == null) return; meeting.days.forEach((day) => { - const crnPeriodInfos = Object.values(crnSizeInfo) - .flatMap((days) => - days != null ? Object.values(days[day] ?? {}) : [] - ) - .flatMap((info) => (info == null ? [] : [info])); - - const eventPeriodInfos = Object.values(eventSizeInfo) - .flatMap((days) => - days != null ? Object.values(days[day] ?? {}) : [] + const dayPeriodInfos = Object.values(meetingSizeInfo) + .flatMap( + (days) => (days != null ? Object.values(days[day] ?? {}) : []) ) - .flatMap((info) => (info == null ? [] : [info])); - - const dayPeriodInfos: (SectionBlockPosition | EventBlockPosition)[] = - crnPeriodInfos; - dayPeriodInfos.push(...eventPeriodInfos); + .flatMap((info) => + info == null ? [] : [info] + ); const curRowSize = dayPeriodInfos .filter( @@ -176,13 +270,13 @@ export default function Calendar({ curRowSize ); - if (!meeting.event) { - const courseSizeInfo = crnSizeInfo[meeting.id] || {}; - crnSizeInfo[meeting.id] = courseSizeInfo; + const mSizeInfo = meetingSizeInfo[meeting.id] || {}; + meetingSizeInfo[meeting.id] = mSizeInfo; - const daySizeInfo = courseSizeInfo[day] || {}; - courseSizeInfo[day] = daySizeInfo; + const daySizeInfo = mSizeInfo[day] || {}; + mSizeInfo[day] = daySizeInfo; + if (!meeting.event) { daySizeInfo[makeSizeInfoKey(period)] = { period, crn: meeting.id, @@ -190,13 +284,7 @@ export default function Calendar({ rowSize: curRowSize, }; } else { - const evtSizeInfo = eventSizeInfo[meeting.id] || {}; - eventSizeInfo[meeting.id] = evtSizeInfo; - - const eventDaySizeInfo = evtSizeInfo[day] || {}; - evtSizeInfo[day] = eventDaySizeInfo; - - eventDaySizeInfo[makeSizeInfoKey(meeting.period)] = { + daySizeInfo[makeSizeInfoKey(period)] = { period: meeting.period, id: meeting.id, rowIndex: curRowSize - 1, @@ -285,11 +373,12 @@ export default function Calendar({ {pinnedCrnsByFirstMeeting.map((crn) => ( ( ))} {events && events.map((event) => ( ))} + {compare && + userSchedules.map(({ data, overlay }) => ( + { + if (meeting === null) { + setSelectedMeeting(null); + } else { + setSelectedMeeting([ + `${data.scheduleId}-${data.crn}`, + meeting[0], + meeting[1], + ]); + } + }} + deviceHasHover={deviceHasHover} + canBeTabFocused={!isAutosized && !capture} + /> + ))} + {compare && + userEvents.map(({ data, overlay }) => ( + { + if (meeting === null) { + setSelectedMeeting(null); + } else { + setSelectedMeeting([ + `${data.scheduleId}-${data.id}`, + meeting[0], + meeting[1], + ]); + } + }} + /> + ))}
    {!preview && hiddenSections.length > 0 && (
    diff --git a/src/components/CombinationContainer/index.tsx b/src/components/CombinationContainer/index.tsx index 0a8adb2a..4cab9168 100644 --- a/src/components/CombinationContainer/index.tsx +++ b/src/components/CombinationContainer/index.tsx @@ -20,7 +20,13 @@ import './stylesheet.scss'; const List = _List as unknown as React.ComponentType; const AutoSizer = _AutoSizer as unknown as React.ComponentType; -export default function CombinationContainer(): React.ReactElement { +export type ComparisonPanelProps = { + compare?: boolean; +}; + +export default function CombinationContainer({ + compare = false, +}: ComparisonPanelProps): React.ReactElement { const [ { oscar, @@ -45,70 +51,87 @@ export default function CombinationContainer(): React.ReactElement { [oscar, desiredCourses, pinnedCrns, excludedCrns, events] ); const sortedCombinations = useMemo( - () => oscar.sortCombinations(combinations, sortingOptionIndex), - [oscar, combinations, sortingOptionIndex] + () => oscar.sortCombinations(combinations, sortingOptionIndex, events), + [oscar, combinations, sortingOptionIndex, events] ); return ( <>
    - + patchSchedule({ sortingOptionIndex: newSortingOptionIndex }) + } + current={sortingOptionIndex} + options={oscar.sortingOptions.map((sortingOption, i) => ({ + id: i, + label: sortingOption.label, + }))} + /> + +
    + + {({ width, height }): React.ReactElement => ( + { + const { crns } = sortedCombinations[index] as Combination; + return ( +
    +
    setOverlayCrns(crns)} + onMouseLeave={(): void => setOverlayCrns([])} + onClick={(): void => + patchSchedule({ + pinnedCrns: [...pinnedCrns, ...crns], + }) + } + > +
    {index + 1}
    + +
    +
    + ); + }} + /> + )} +
    +
    + + )}
    void; +}; + +export default function CompareBlocks({ + className, + owner, + scheduleId, + scheduleName, + crn, + overlay = false, + capture, + sizeInfo, + includeDetailsPopover, + includeContent, + canBeTabFocused = false, + deviceHasHover = true, + selectedMeeting, + onSelectMeeting, +}: CompareBlocksProps): React.ReactElement | null { + const [{ oscar }] = useContext(ScheduleContext); + + const section = oscar.findSection(crn); + if (section == null) return null; + + return ( +
    + {section.meetings.map((meeting, i) => { + const { period } = meeting; + if (period == null) return; + + return ( + + ); + })} +
    + ); +} diff --git a/src/components/CompareBlocks/stylesheet.scss b/src/components/CompareBlocks/stylesheet.scss new file mode 100644 index 00000000..37f39ea9 --- /dev/null +++ b/src/components/CompareBlocks/stylesheet.scss @@ -0,0 +1,16 @@ +.mobile .TimeBlocks:not(.capture) .meeting .meeting-wrapper { + .ids { + .course-id { + flex: 1; + } + + .section-id { + display: none; + } + } + + .where, + .instructors { + display: none; + } +} \ No newline at end of file diff --git a/src/components/ComparisonContainer/index.tsx b/src/components/ComparisonContainer/index.tsx new file mode 100644 index 00000000..333ef66d --- /dev/null +++ b/src/components/ComparisonContainer/index.tsx @@ -0,0 +1,876 @@ +import React, { + useState, + useContext, + useCallback, + useId, + useEffect, +} from 'react'; +import { + faPencil, + faCircleXmark, + faXmark, + faPalette, + faShareFromSquare, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import axios from 'axios'; + +import { classes, getRandomColor } from '../../utils/misc'; +import { + ScheduleContext, + FriendContext, + AccountContext, + SignedIn, +} from '../../contexts'; +import Button from '../Button'; +import Modal from '../Modal'; +import { AutoFocusInput } from '../Select'; +import { Palette } from '..'; +import { ErrorWithFields, softError } from '../../log'; +import { CLOUD_FUNCTION_BASE_URL } from '../../constants'; +import InvitationModal from '../InvitationModal'; +import ComparisonContainerShareBack from '../ComparisonContainerShareBack/ComparisonContainerShareBack'; +import { ScheduleDeletionRequest } from '../../types'; + +import './stylesheet.scss'; + +export type SharedSchedule = { + email: string; + name: string; + schedules: { + id: string; + name: string; + color: string; + }[]; +}; + +export type DeleteInfo = { + id: string; + type: string; + name: string; + owner?: string; + ownerName?: string; +} | null; + +export type EditInfo = { + id: string; + owner?: string; + type: string; +} | null; + +export type ComparisonContainerProps = { + handleCompareSchedules: ( + compare?: boolean, + pinnedSchedules?: string[], + pinSelf?: boolean, + expanded?: boolean, + overlaySchedules?: string[] + ) => void; + pinnedSchedules: string[]; + shareBackRemount: number; +}; + +export default function ComparisonContainer({ + handleCompareSchedules, + pinnedSchedules, + shareBackRemount, +}: ComparisonContainerProps): React.ReactElement { + const [selected, setSelected] = useState(pinnedSchedules); + const [deleteConfirm, setDeleteConfirm] = useState(null); + const [editInfo, setEditInfo] = useState(null); + const [editValue, setEditValue] = useState(''); + const [paletteInfo, setPaletteInfo] = useState(); + const [invitationModalOpen, setInvitationModalOpen] = useState(false); + const [invitationModalEmail, setInvitationModalEmail] = useState(''); + + const [ + { allVersionNames, currentVersion, colorMap, term }, + { deleteVersion, renameVersion, patchSchedule }, + ] = useContext(ScheduleContext); + + const [{ friends }, { renameFriend }] = useContext(FriendContext); + + const accountContext = useContext(AccountContext); + + useEffect(() => { + const newColorMap = { ...colorMap }; + allVersionNames.forEach((versionName) => { + const version = versionName.id; + if (!(version in newColorMap)) { + newColorMap[version] = getRandomColor(); + } + }); + if (!(currentVersion in newColorMap)) { + newColorMap[currentVersion] = getRandomColor(); + } + Object.entries(friends).forEach((friend) => { + if (!(friend[0] in newColorMap)) { + newColorMap[friend[0]] = getRandomColor(); + } + Object.keys(friend[1].versions).forEach((schedule) => { + if (!(schedule in newColorMap)) { + newColorMap[schedule] = getRandomColor(); + } + }); + }); + if (Object.keys(newColorMap).length !== Object.keys(colorMap).length) { + patchSchedule({ colorMap: newColorMap }); + } + }, [friends, currentVersion, colorMap, patchSchedule, allVersionNames]); + + const handleEdit = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + if (editValue.trim() === '') return; + if (editInfo?.type === 'Version') { + renameVersion(editInfo?.id, editValue.trim()); + } else if (editInfo?.type === 'User') { + renameFriend(editInfo?.id, editValue.trim()); + } + setEditInfo(null); + setEditValue(''); + } + + if (e.key === 'Escape') { + setEditInfo(null); + setEditValue(''); + } + }, + [editInfo, editValue, renameVersion, renameFriend] + ); + + const handleNameEditOnBlur = useCallback(() => { + if (editValue.trim() === '') return; + if (editInfo?.type === 'User') { + renameFriend(editInfo?.id, editValue.trim()); + } + if (editInfo?.type === 'Version') { + renameVersion(editInfo?.id, editValue.trim()); + } + setEditInfo(null); + setEditValue(''); + }, [editInfo, editValue, renameFriend, renameVersion]); + + const deleteSchedulesFromInvitee = useCallback( + async (senderId: string, versions: string[]) => { + const data = JSON.stringify({ + IDToken: await (accountContext as SignedIn).getToken(), + peerUserId: senderId, + term, + versions, + owner: false, + } as ScheduleDeletionRequest); + + const friend = friends[senderId]; + if (friend) { + axios + .post( + `${CLOUD_FUNCTION_BASE_URL}/deleteSharedSchedule`, + `data=${data}`, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ) + .then(() => { + const newColorMap = { ...colorMap }; + versions.forEach((schedule) => { + delete newColorMap[schedule]; + }); + setSelected( + selected.filter( + (selectedId: string) => + !Object.keys(friend.versions).includes(selectedId) + ) + ); + patchSchedule({ colorMap: newColorMap }); + // updateFriendTermData((draft) => { + // delete draft.accessibleSchedules[senderId]; + // }); + }) + .catch((err) => { + throw err; + }); + } + }, + [accountContext, term, colorMap, friends, patchSchedule, selected] + ); + + // remove all versions of a particular friend from user (invitee) view + const handleRemoveFriend = useCallback( + (ownerId: string) => { + const friend = friends[ownerId]; + if (friend) { + const versions = Object.keys(friend.versions); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + deleteSchedulesFromInvitee(ownerId, versions).catch((err) => { + softError( + new ErrorWithFields({ + message: 'Failed to delete user schedule', + source: err, + fields: { + user: (accountContext as SignedIn).id, + sender: ownerId, + term, + versions, + }, + }) + ); + }); + } + }, + [friends, deleteSchedulesFromInvitee, accountContext, term] + ); + + const handleRemoveSchedule = useCallback( + (id: string, ownerId: string) => { + deleteSchedulesFromInvitee(ownerId, [id]).catch((err) => { + softError( + new ErrorWithFields({ + message: 'Failed to delete user schedule', + source: err, + fields: { + user: (accountContext as SignedIn).id, + sender: ownerId, + term, + versions: [id], + }, + }) + ); + }); + }, + [deleteSchedulesFromInvitee, accountContext, term] + ); + + const handleToggleSchedule = useCallback( + (id: string) => { + if (selected.includes(id)) { + setSelected(selected.filter((selectedId: string) => selectedId !== id)); + handleCompareSchedules( + undefined, + selected.filter((selectedId: string) => selectedId !== id), + undefined + ); + } else { + setSelected(selected.concat([id])); + handleCompareSchedules(undefined, selected.concat([id]), undefined); + } + }, + [selected, handleCompareSchedules] + ); + + const setFriendScheduleColor = useCallback( + (color: string, id: string) => { + const newColorMap = { ...colorMap }; + newColorMap[id] = color; + patchSchedule({ colorMap: newColorMap }); + }, + [colorMap, patchSchedule] + ); + + const sortedFriendsArray = Object.entries(friends).sort( + ([, friendA], [, friendB]) => friendA.name.localeCompare(friendB.name) + ); + + return ( +
    + { + setInvitationModalOpen(false); + }} + inputEmail={invitationModalEmail} + /> +
    +
    +
    +

    My Schedule

    + {allVersionNames + // .filter((version) => version.id === currentVersion) + .map((version) => { + return ( + { + handleToggleSchedule(version.id); + }} + checkboxColor={ + selected.includes(version.id) ? colorMap[version.id] : '' + } + name={version.name} + // placeholder functions + handleEditSchedule={(): void => { + setEditInfo({ + id: version.id, + type: 'Version', + }); + setEditValue(version.name); + }} + handleRemoveSchedule={(): void => { + setDeleteConfirm({ + id: version.id, + type: 'Version', + name: version.name, + }); + }} + hasDelete={allVersionNames.length >= 2} + editOnChange={( + e: React.ChangeEvent + ): void => setEditValue(e.target.value)} + editOnKeyDown={handleEdit} + editInfo={editInfo} + setEditInfo={setEditInfo} + editValue={editValue} + hasPalette + setFriendScheduleColor={(color: string): void => { + setFriendScheduleColor(color, version.id); + }} + color={colorMap[version.id]} + paletteInfo={paletteInfo} + setPaletteInfo={setPaletteInfo} + handleNameEditOnBlur={handleNameEditOnBlur} + hoverFriendSchedule={(): void => { + handleCompareSchedules( + undefined, + undefined, + undefined, + undefined, + [version.id] + ); + }} + unhoverFriendSchedule={(): void => { + handleCompareSchedules( + undefined, + undefined, + undefined, + undefined, + [] + ); + }} + /> + ); + })} +
    +
    +

    Shared with me

    + {Object.keys(friends).length !== 0 ? ( + sortedFriendsArray.map(([friendId, friend]) => { + return ( +
    + { + setEditInfo({ + id: friendId, + type: 'User', + }); + setEditValue(friend.name); + }} + handleRemoveSchedule={(): void => { + setDeleteConfirm({ + id: friendId, + type: 'User', + name: friend.name, + }); + }} + hasTooltip + editOnChange={( + e: React.ChangeEvent + ): void => setEditValue(e.target.value)} + editOnKeyDown={handleEdit} + editInfo={editInfo} + setEditInfo={setEditInfo} + editValue={editValue} + setInvitationModalEmail={setInvitationModalEmail} + setInvitationModalOpen={setInvitationModalOpen} + handleNameEditOnBlur={handleNameEditOnBlur} + /> +
    +

    {friend.email}

    +
    + {Object.entries(friend.versions).map( + ([scheduleId, schedule]) => { + return ( + + handleToggleSchedule(scheduleId) + } + checkboxColor={ + selected.includes(scheduleId) + ? colorMap[scheduleId] + : '' + } + name={schedule.name} + handleEditSchedule={(): void => { + setEditInfo({ + id: scheduleId, + owner: friendId, + type: 'Schedule', + }); + setEditValue(schedule.name); + }} + handleRemoveSchedule={(): void => { + setDeleteConfirm({ + id: scheduleId, + type: 'Schedule', + name: schedule.name, + owner: friendId, + ownerName: friend.name, + }); + }} + hasPalette + hasEdit={false} + setFriendScheduleColor={(color: string): void => { + setFriendScheduleColor(color, scheduleId); + }} + color={colorMap[scheduleId]} + paletteInfo={paletteInfo} + setPaletteInfo={setPaletteInfo} + hoverFriendSchedule={(): void => { + handleCompareSchedules( + undefined, + undefined, + undefined, + undefined, + [scheduleId] + ); + }} + unhoverFriendSchedule={(): void => { + handleCompareSchedules( + undefined, + undefined, + undefined, + undefined, + [] + ); + }} + handleNameEditOnBlur={handleNameEditOnBlur} + /> + ); + } + )} + +
    + ); + }) + ) : ( +
    +

    + No schedules are currently shared with you. +

    +

    + Accept invitations from other users to see their schedules on + this view. +

    +
    + )} +
    + +
    +
    +
    + ); +} + +type ScheduleRowProps = { + id: string; + type: string; + owner?: string; + hasCheck?: boolean; + onClick?: () => void; + checkboxColor?: string; + email?: string; + name: string; + handleEditSchedule: () => void; + handleRemoveSchedule: () => void; + setInvitationModalOpen?: React.Dispatch>; + setInvitationModalEmail?: React.Dispatch>; + hasPalette?: boolean; + hasEdit?: boolean; + hasDelete?: boolean; + hasTooltip?: boolean; + setFriendScheduleColor?: (color: string) => void; + color?: string; + paletteInfo?: string; + setPaletteInfo?: (info: string) => void; + editOnChange?: (e: React.ChangeEvent) => void; + editOnKeyDown?: (e: React.KeyboardEvent) => void; + editInfo?: EditInfo; + setEditInfo?: (info: EditInfo) => void; + editValue?: string; + hoverFriendSchedule?: () => void; + unhoverFriendSchedule?: () => void; + handleNameEditOnBlur?: () => void; +}; + +function ScheduleRow({ + id, + type, + owner, + hasCheck = true, + onClick, + checkboxColor, + email, + name, + handleEditSchedule, + handleRemoveSchedule, + hasPalette = false, + hasEdit = true, + hasDelete = true, + hasTooltip = false, + setFriendScheduleColor, + color, + paletteInfo, + setPaletteInfo, + editOnChange, + editOnKeyDown, + editInfo, + setEditInfo, + editValue, + setInvitationModalOpen, + setInvitationModalEmail, + hoverFriendSchedule, + unhoverFriendSchedule, + handleNameEditOnBlur, +}: ScheduleRowProps): React.ReactElement { + const tooltipId = useId(); + const [tooltipHover, setTooltipHover] = useState(false); + const [divHover, setDivHover] = useState(false); + const [showPaletteTooltip, setShowPaletteTooltip] = useState(false); + const [showShareTooltip, setShowShareTooltip] = useState(false); + const [showEditTooltip, setShowEditTooltip] = useState(false); + const [showRemoveTooltip, setShowRemoveTooltip] = useState(false); + + const edit = + hasEdit && + editInfo != null && + editInfo.type === type && + editInfo.id === id && + editInfo.owner === owner; + + const palette = hasPalette && paletteInfo === id; + + return ( +
    { + if (type === 'Schedule' || type === 'Version') { + hoverFriendSchedule?.(); + } + }} + onMouseLeave={(): void => { + if (type === 'Schedule' || type === 'Version') { + unhoverFriendSchedule?.(); + } + }} + > +
    setDivHover(true)} + onMouseLeave={(): void => setDivHover(false)} + > + {hasCheck && ( +
    + )} + {setEditInfo && edit && ( + + )} + {!edit && ( + <> +
    setTooltipHover(true)} + onMouseLeave={(): void => setTooltipHover(false)} + onClick={onClick} + > +
    +

    {name}

    +
    + {hasTooltip && email !== name && ( + +

    {email}

    +
    + )} +
    +
    + + )} + {(divHover || edit) && hasPalette && setPaletteInfo && ( +
    setShowPaletteTooltip(true)} + onMouseLeave={(): void => setShowPaletteTooltip(false)} + id={`${tooltipId}-palette`} + > + + + Edit Color + +
    + )} + {(divHover || edit) && + hasEdit && + setInvitationModalOpen !== undefined && + setInvitationModalEmail !== undefined && + email && ( +
    setShowShareTooltip(true)} + onMouseLeave={(): void => setShowShareTooltip(false)} + id={`${tooltipId}-share`} + > + + + Share Back + +
    + )} + {(divHover || edit) && hasEdit && ( +
    setShowEditTooltip(true)} + onMouseLeave={(): void => setShowEditTooltip(false)} + id={`${tooltipId}-edit`} + > + + + Edit + +
    + )} + {(divHover || edit) && hasDelete && ( +
    setShowRemoveTooltip(true)} + onMouseLeave={(): void => setShowRemoveTooltip(false)} + id={`${tooltipId}-delete`} + > + + + Remove + +
    + )} +
    + {hasPalette && palette && setFriendScheduleColor && setPaletteInfo && ( + setPaletteInfo('')} + /> + )} +
    + ); +} + +type ComparisonModalProps = { + deleteConfirm: DeleteInfo; + setDeleteConfirm: (deleteConfirm: DeleteInfo) => void; + deleteVersion: (id: string) => void; + handleRemoveFriend: (id: string) => void; + handleRemoveSchedule: (id: string, owner: string) => void; +}; + +function ComparisonModal({ + deleteConfirm, + setDeleteConfirm, + deleteVersion, + handleRemoveFriend, + handleRemoveSchedule, +}: ComparisonModalProps): React.ReactElement { + return ( + setDeleteConfirm(null)} + buttons={[ + { + label: 'Remove', + onClick: (): void => { + if (deleteConfirm != null) { + if (deleteConfirm.type === 'Version') { + deleteVersion(deleteConfirm.id); + } else if (deleteConfirm.type === 'User') { + handleRemoveFriend(deleteConfirm.id); + } else { + handleRemoveSchedule( + deleteConfirm.id, + deleteConfirm.owner ?? '' + ); + } + } + setDeleteConfirm(null); + }, + }, + ]} + preserveChildrenWhileHiding + > + + {deleteConfirm?.type === 'Version' && ( +
    +

    Delete confirmation

    +

    + Are you sure you want to delete schedule “ + {deleteConfirm?.name ?? ''}”? +

    +
    + )} + {deleteConfirm?.type === 'User' && ( +
    +

    Remove User

    +

    + Are you sure you want to remove the following user's schedules + from your view? +

    +

    + User: {deleteConfirm?.name} +

    +

    + You will not be able to see any of their schedules unless the owner + sends another invite for each one. +

    +
    + )} + {deleteConfirm?.type === 'Schedule' && ( +
    +

    Remove Schedule

    +

    + Are you sure you want to remove the following schedule from your + view? +

    +

    + Schedule: {deleteConfirm?.name}
    + Owner: {deleteConfirm?.ownerName} +

    +

    + You will not be able to see it unless the owner sends another + invite. +

    +
    + )} +
    + ); +} diff --git a/src/components/ComparisonContainer/stylesheet.scss b/src/components/ComparisonContainer/stylesheet.scss new file mode 100644 index 00000000..4f68ce89 --- /dev/null +++ b/src/components/ComparisonContainer/stylesheet.scss @@ -0,0 +1,298 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); +@import '../../variables'; + +.comparison-container { + height: auto; + .comparison-body { + display: flex; + flex-direction: column; + + .comparison-content { + position: relative; + overflow-x: hidden; + overflow-y: auto; + flex: 1 1 auto; + + p { + margin: 0px; + overflow: hidden; + font-size: 14px; + font-weight: 400; + line-height: normal; + + &.content-title { + font-weight: 700; + margin: 16px 12px 0px 12px; + border-bottom: 2px solid $color-border; + } + + &.my-schedule-title { + font-weight: 700; + margin: 0px 12px 0px 12px; + border-bottom: 2px solid $color-border; + } + + &.shared-with { + margin-bottom: 4px; + } + } + + .friend { + padding-bottom: 10px; + } + + .friend-email { + display: flex; + align-items: center; + p { + padding: 0px 2px 2px 12px; + text-align: center; + } + } + + .checked { + p { + font-weight: 700; + font-size: 14px; + } + } + + .schedule-name { + cursor: pointer; + } + + .friend-name { + p { + font-weight: 575; + font-size: 16px; + } + } + + .no-shared-schedules { + margin: 8px 12px; + + p { + font-size: 13px; + font-weight: 400; + font-style: italic; + white-space: pre-wrap; + } + + & > p { + margin: 0px 0px 6px; + } + } + + .schedule-row { + .checkbox-container { + display: flex; + align-items: center; + position: relative; + height: 22px; + + .checkbox { + margin-left: 12px; + border: 1px solid; + border-radius: 3px; + border-color: var(--theme-fg); + transition-duration: $theme-switch-transition-duration; + transition-property: border-color; + + width: 12px; + height: 12px; + + &:hover { + cursor: pointer; + } + + &.indented { + margin-left: 24px; + } + } + + .name { + margin-left: 12px; + margin-right: 12px; + min-width: 0; + flex-shrink: 1; + + &.check { + margin-left: 8px; + } + + p { + text-overflow: ellipsis; + } + } + + .edit-input { + height: 22px; + border-radius: 4px; + padding: 4px; + flex: 1 1; + font-size: 12px; + font-weight: 500; + outline: none; + border: 1px solid var(--theme-fg); + margin-left: 8px; + min-width: 0px; + + &.check { + margin-left: 4px; + } + } + + .spacing { + flex: 1; + } + + .tooltip { + background: rgba(0, 0, 0, 0.8); + border-radius: 4px; + } + + .icon { + width: 27px; + height: 22px; + padding: 0px; + opacity: 0; + } + + &:hover { + background-color: $color-border; + + .icon { + opacity: 1; + } + + .name { + margin-right: 0px; + } + } + + &.editing { + .icon { + opacity: 1; + } + } + + &.schedule-checkbox { + margin-top: 4px; + margin-bottom: 2px; + } + } + + .palette { + margin: 0px 12px; + height: 50px; + border-radius: 4px; + overflow: hidden; + &.indented { + margin-left: 24px; + } + } + } + + .shareback-panel { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 12px; + margin-left: 10px; + margin-right: 10px; + border: 1px solid $color-neutral; + border-radius: 8px; + padding: 8px 10px; + gap: 6px; + + p { + font-size: 12px; + line-height: normal; + font-style: oblique; + overflow: visible; + } + + .shareback-button { + width: 100px; + height: 24px; + padding: 6px 10px; + font-size: 9px; + color: var(--theme-bg); + background-color: var(--theme-fg); + border-radius: 8px; + border: 1px solid rgba(128, 128, 128, 0.2); + margin-left: 5px; + font-weight: 650; + cursor: pointer; + + &:hover { + @include dark { + background: $modal-foreground-color-dark; + } + + @include light { + background: $modal-foreground-color-light; + } + } + } + + .dont-shareback-button { + width: 100px; + height: 24px; + padding: 6px 10px; + font-size: 9px; + color: var(--theme-fg); + background-color: var(--theme-bg); + border-radius: 8px; + border: 1px solid $color-neutral; + margin-right: 5px; + font-weight: 650; + cursor: pointer; + + &:hover { + @include dark { + background: $modal-foreground-color-light; + } + + @include light { + background: $modal-foreground-color-dark; + } + } + } + } + } + } +} + +.mobile .comparison-container { + flex: 1; + border-right: none; + + .scroller { + width: auto; + } +} + +.shared-schedule-modal { + h2 { + font-weight: 700; + font-size: 24px; + line-height: 28px; + } + + p { + font-size: 14px; + line-height: 18px; + overflow-wrap: break-word; + } + + .cancel-button { + width: 26px; + height: 26px; + position: absolute; + top: 11px; + right: 11px; + border-radius: 50%; + color: $color-neutral; + } +} diff --git a/src/components/ComparisonContainerShareBack/ComparisonContainerShareBack.tsx b/src/components/ComparisonContainerShareBack/ComparisonContainerShareBack.tsx new file mode 100644 index 00000000..4e1a1b8b --- /dev/null +++ b/src/components/ComparisonContainerShareBack/ComparisonContainerShareBack.tsx @@ -0,0 +1,89 @@ +import React, { useContext, useMemo } from 'react'; +import useLocalStorageState from 'use-local-storage-state'; + +import { ScheduleContext } from '../../contexts'; + +type ComparisonContainerShareBack = { + friendId: string; + friendName: string; + friendEmail: string; + setModalEmail: React.Dispatch>; + setModalOpen: React.Dispatch>; +}; + +export default function ComparisonContainerShareBack({ + friendName, + friendEmail, + friendId, + setModalEmail, + setModalOpen, +}: ComparisonContainerShareBack): React.ReactElement | null { + const [{ allFriends, allVersionNames }] = useContext(ScheduleContext); + + const [hasSeen, setHasSeen] = useLocalStorageState( + `share-back-invitation-${friendId}`, + { + defaultValue: false, + storageSync: true, + } + ); + + const schedulesShared = useMemo(() => { + return Object.keys(allFriends) + .map((version_id) => { + if ( + friendId && + allFriends[version_id] && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + friendId in allFriends[version_id]! + ) { + const versionName = allVersionNames.filter( + (v) => v.id === version_id + ); + if (versionName.length > 0) { + return versionName[0]?.name; + } + } + return undefined; + }) + .filter((v) => v) as string[]; + }, [friendId, allFriends, allVersionNames]); + + if (hasSeen || schedulesShared.length === allVersionNames.length) { + return null; + } + + return ( +
    +
    +

    + You have {friendName}'s schedule. Would you like + to share yours back? +

    +
    +
    + + + +
    +
    + ); +} diff --git a/src/components/ComparisonPanel/index.tsx b/src/components/ComparisonPanel/index.tsx new file mode 100644 index 00000000..ad0a5a3f --- /dev/null +++ b/src/components/ComparisonPanel/index.tsx @@ -0,0 +1,176 @@ +import React, { useState, useContext, useId, useCallback } from 'react'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import { faShare } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { CombinationContainer, ComparisonContainer } from '..'; +import { AccountContext } from '../../contexts/account'; +import { classes } from '../../utils/misc'; +import InvitationModal from '../InvitationModal'; +import LoginModal from '../LoginModal'; +import InvitationAcceptModal from '../InvitationAcceptModal/InvitationAcceptModal'; + +import './stylesheet.scss'; + +export type ComparisonPanelProps = { + handleCompareSchedules: ( + compare?: boolean, + pinnedSchedules?: string[], + pinSelf?: boolean, + expanded?: boolean, + overlaySchedules?: string[] + ) => void; + pinnedSchedules: string[]; + compare: boolean; + expanded: boolean; +}; + +export default function ComparisonPanel({ + handleCompareSchedules, + pinnedSchedules, + compare, + expanded, +}: ComparisonPanelProps): React.ReactElement { + const [hover, setHover] = useState(false); + const [tooltipY, setTooltipY] = useState(0); + const [invitationOpen, setInvitationOpen] = useState(false); + // const [hoverCompare, setHoverCompare] = useState(false); + // const [tooltipYCompare, setTooltipYCompare] = useState(0); + const tooltipId = useId(); + const [loginOpen, setLoginOpen] = useState(false); + const hideLogin = useCallback(() => setLoginOpen(false), []); + + const hideInvitation = useCallback(() => setInvitationOpen(false), []); + + const { type } = useContext(AccountContext); + + const handleHover = useCallback((e: React.MouseEvent) => { + setHover(true); + setTooltipY(e.clientY); + }, []); + + const handleOpenInvitation = useCallback(() => { + if (type === 'signedIn') { + setInvitationOpen(true); + } else { + setLoginOpen(true); + } + }, [type]); + + const handleTogglePanel = useCallback(() => { + if (type === 'signedIn') { + handleCompareSchedules(!compare, undefined, undefined); + } else { + setLoginOpen(true); + } + }, [type, compare, handleCompareSchedules]); + + const [shareBackRemount, setShareBackRemount] = useState(0); + + return ( +
    + +
    { + handleCompareSchedules(undefined, undefined, undefined, !expanded); + setHover(false); + }} + onMouseEnter={(e: React.MouseEvent): void => { + handleHover(e); + }} + onMouseLeave={(): void => setHover(false)} + id={tooltipId} + > +
    +
    +
    +
    +
    + +

    {expanded ? 'Collapse' : 'Expand for More Options'}

    +
    +
    +
    + +
    + +
    +
    +

    Compare Schedules

    +

    {compare ? 'On' : 'Off'}

    +
    + ); +} diff --git a/src/components/ComparisonPanel/stylesheet.scss b/src/components/ComparisonPanel/stylesheet.scss new file mode 100644 index 00000000..d2b99e01 --- /dev/null +++ b/src/components/ComparisonPanel/stylesheet.scss @@ -0,0 +1,234 @@ +@import '../../variables'; + +.comparison-panel { + display: flex; + flex-direction: row; + + .drawer { + width: 13px; + border-width: 0px 0px 0px 1px; + border-color: rgba(255, 255, 255, 0.5); + border-style: solid; + display: flex; + flex-direction: column; + align-items: center; + + &.opened { + border-color: $color-border; + border-width: 0px 1px 0px 1px; + } + + &:hover { + background: $color-border; + } + + .drawer-line { + flex: 1; + width: 5px; + border-width: 0px 1px 0px 1px; + border-color: rgba(255, 255, 255, 0.5); + border-style: solid; + &.opened { + border-color: $color-border; + } + } + + .icon { + display: flex; + align-items: center; + height: 48px; + transform: scale(0.9, 1.5); + + .arrow { + position: absolute; + height: 10px; + width: 10px; + right: -8px; + border-width: 0px 2px 2px 0px; + border-color: rgba(255, 255, 255, 1); + border-style: solid; + transform: rotate(135deg); + + &.right { + left: -8px; + transform: rotate(-45deg); + border-color: $color-border; + } + } + } + + .tooltip { + background: black; + border-radius: 4px; + z-index: 10; + + p { + margin: 0px; + font-size: 12px; + font-weight: 400; + } + } + } + + .panel { + display: flex; + flex: 0 0 1; + flex-direction: column; + align-items: stretch; + width: 256px; + transition: width 0.15s; + overflow-y: auto; + + .comparison-header { + display: flex; + align-items: center; + margin: 11px 12px 20px 12px; + + p { + margin: 0px; + overflow: hidden; + + &.header-title { + flex: 1; + font-size: 16px; + } + + &.header-text { + font-size: 12px; + margin-right: 4px; + } + } + + .switch { + display: inline-block; + height: 19px; + position: relative; + width: 43px; + } + + .switch input { + display: none; + } + + .slider { + background-color: $color-neutral; + bottom: 0; + cursor: pointer; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: 0.4s; + border-radius: 19px; + } + + .slider:before { + background-color: #ffffff; + bottom: 2px; + content: ''; + height: 15px; + left: 2px; + position: absolute; + transition: 0.4s; + width: 15px; + border-radius: 50%; + } + + input.checked + .slider { + background-color: #589bd5; + } + + input.checked + .slider:before { + transform: translateX(24px); + } + } + + .comparison-overlay { + background-color: var(--theme-bg); + + pointer-events: none; + opacity: 0; + transition-duration: 0.15s, $theme-switch-transition-duration, + $theme-switch-transition-duration; + transition-property: opacity, color, background-color; + + &.left { + position: fixed; + top: 64px; + left: 0px; + width: 320px; + bottom: 41px; + } + + &.right { + position: absolute; + top: 100px; + width: 256px; + height: 360px; + } + + &.open { + pointer-events: all; + opacity: 0.4; + } + } + + .overlay-tooltip { + background: rgba(0, 0, 0, 0.8); + border-radius: 4px; + font-size: 12px; + text-align: center; + p { + margin: 0px; + } + } + + &.closed { + width: 0px; + } + + .combination { + flex: 1; + + .content-title { + font-weight: 700; + font-size: 14px; + margin: 18px 12px 4px 12px; + border-bottom: 2px solid $color-border; + } + + .CombinationContainer { + height: 100%; + } + } + } + .invite-panel { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + + .invite-button { + align-items: center; + font-size: 14px; + font-weight: 700; + color: var(--theme-fg); + border-radius: 10px; + background-color: $color-border; + display: flex; + padding: 10px 47px 10px 47px; + border: none; + gap: 5px; + + &:hover { + @include dark { + background: $modal-foreground-color-light; + } + + @include light { + background: $modal-foreground-color-dark; + } + } + } + } +} diff --git a/src/components/CourseAdd/stylesheet.scss b/src/components/CourseAdd/stylesheet.scss index d7e9070f..82937869 100644 --- a/src/components/CourseAdd/stylesheet.scss +++ b/src/components/CourseAdd/stylesheet.scss @@ -73,4 +73,4 @@ padding: 4px; font-size: .8em; } -} +} \ No newline at end of file diff --git a/src/components/CourseContainer/stylesheet.scss b/src/components/CourseContainer/stylesheet.scss index 6e2b4ab6..b82aae35 100644 --- a/src/components/CourseContainer/stylesheet.scss +++ b/src/components/CourseContainer/stylesheet.scss @@ -28,7 +28,7 @@ .updated-at { color: $color-neutral; - font-size: .8em; + font-size: 0.8em; } } diff --git a/src/components/CourseNavMenu/index.tsx b/src/components/CourseNavMenu/index.tsx index 07a74b70..1596bbaa 100644 --- a/src/components/CourseNavMenu/index.tsx +++ b/src/components/CourseNavMenu/index.tsx @@ -20,6 +20,7 @@ export default function CourseNavMenu({
    {items.map((item, idx) => ( onChangeItem(idx)} diff --git a/src/components/DonateBanner/index.tsx b/src/components/DonateBanner/index.tsx new file mode 100644 index 00000000..a8613ab7 --- /dev/null +++ b/src/components/DonateBanner/index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import useLocalStorageState from 'use-local-storage-state'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import Button from '../Button'; +import { DESKTOP_BREAKPOINT } from '../../constants'; +import useScreenWidth from '../../hooks/useScreenWidth'; + +import './stylesheet.scss'; + +const BANNER_LOCAL_STORAGE_KEY = '2024-04-01-spr2024-donate-banner'; + +export default function DonateBanner(): React.ReactElement { + const [hasSeen, setHasSeen] = useLocalStorageState(BANNER_LOCAL_STORAGE_KEY, { + defaultValue: false, + storageSync: true, + }); + const mobile = !useScreenWidth(DESKTOP_BREAKPOINT); + + return ( +
    + {!hasSeen ? ( +
    +
    + + {!mobile + ? 'Help keep GT Scheduler and its amazing features running!' + : 'Help us and'} + + + {!mobile ? 'Donate today.' : 'donate today.'} + + + + +
    + ) : ( +
    + )} +
    + ); +} diff --git a/src/components/DonateBanner/stylesheet.scss b/src/components/DonateBanner/stylesheet.scss new file mode 100644 index 00000000..ac338e1e --- /dev/null +++ b/src/components/DonateBanner/stylesheet.scss @@ -0,0 +1,23 @@ +.banner { + width: 100%; + height: fit-content; + color: white; + background-color: #C56E5B; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + + .donateButton { + display: inline-block; + padding: 10px 6px; + } + + .donateButton:hover { + text-decoration: underline; + } + + .spacer { + width: 49px; + } +} \ No newline at end of file diff --git a/src/components/Event/index.tsx b/src/components/Event/index.tsx index 3b3e87d5..70e54d12 100644 --- a/src/components/Event/index.tsx +++ b/src/components/Event/index.tsx @@ -4,6 +4,7 @@ import { faPencil, faPalette, faTrash, + faClone, } from '@fortawesome/free-solid-svg-icons'; import { @@ -11,6 +12,7 @@ import { getContentClassName, periodToString, daysToString, + getRandomColor, } from '../../utils/misc'; import { ActionRow, EventAdd, Palette } from '..'; import { ScheduleContext } from '../../contexts'; @@ -31,6 +33,32 @@ export default function Event({ const [{ events, colorMap }, { patchSchedule }] = useContext(ScheduleContext); const [formShown, setFormShown] = useState(false); + const handleDuplicateEvent = useCallback(() => { + const eventId = new Date().getTime().toString(); + const newEvent = { + id: eventId, + name: event.name, + period: { + start: event.period.start, + end: event.period.end, + }, + days: event.days, + }; + + patchSchedule({ + events: [...castDraft(events), castDraft(newEvent)], + colorMap: { ...colorMap, [eventId]: getRandomColor() }, + }); + }, [ + colorMap, + event.days, + event.name, + event.period.end, + event.period.start, + events, + patchSchedule, + ]); + const handleRemoveEvent = useCallback( (id: string) => { const newColorMap = { ...colorMap }; @@ -70,6 +98,12 @@ export default function Event({ id: `${event.id}-color`, onClick: (): void => setPaletteShown(!paletteShown), }, + { + icon: faClone, + tooltip: 'Duplicate Event', + id: `${event.id}-duplicate`, + onClick: (): void => handleDuplicateEvent(), + }, { icon: faTrash, tooltip: `Remove Event`, diff --git a/src/components/EventBlocks/index.tsx b/src/components/EventBlocks/index.tsx index e88af9ce..6f987ea7 100644 --- a/src/components/EventBlocks/index.tsx +++ b/src/components/EventBlocks/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext, useRef } from 'react'; +import React, { useState, useContext, useRef, useEffect } from 'react'; import { Immutable, castDraft } from 'immer'; import { daysToString, periodToString } from '../../utils/misc'; @@ -20,14 +20,18 @@ export interface EventBlockPosition extends TimeBlockPosition { export type EventBlocksProps = { className?: string; event: Immutable; + owner?: string; + scheduleName?: string; + scheduleId?: string; + overlay?: boolean; capture: boolean; includeDetailsPopover: boolean; includeContent: boolean; sizeInfo: SizeInfo; canBeTabFocused?: boolean; deviceHasHover?: boolean; - daysRef: React.RefObject; - timesRef: React.RefObject; + daysRef?: React.RefObject; + timesRef?: React.RefObject; selectedMeeting?: [meetingIndex: number, day: string] | null; onSelectMeeting?: ( meeting: [meetingIndex: number, day: string] | null @@ -37,6 +41,10 @@ export type EventBlocksProps = { export default function EventBlocks({ className, event, + owner, + scheduleName, + scheduleId, + overlay = false, capture, sizeInfo, includeDetailsPopover, @@ -48,6 +56,18 @@ export default function EventBlocks({ selectedMeeting, onSelectMeeting, }: EventBlocksProps): React.ReactElement | null { + const popover = scheduleName + ? [ + { + name: 'Owner', + content: owner, + }, + { + name: 'Schedule', + content: scheduleName, + }, + ] + : []; const [tempStart, setTempStart] = useState(event.period.start); // Store these in refs since the event handlers won't be re generated @@ -55,6 +75,10 @@ export default function EventBlocks({ const tempStartRef = useRef(event.period.start); const tempDaysRef = useRef([...event.days]); + useEffect(() => { + setTempStart(event.period.start); + }, [event.period.start]); + // Save original style of the block const savedStyleRef = useRef(); const savedClassListRef = useRef(); @@ -144,7 +168,7 @@ export default function EventBlocks({ e: MouseEvent, ref: React.RefObject ): void => { - if (!ref.current || !timesRef.current || !daysRef.current) return; + if (!ref.current || !timesRef?.current || !daysRef?.current) return; // math which calculates the new start time by calculating mouse // position proportional to calendar size, then we find new time @@ -213,7 +237,7 @@ export default function EventBlocks({ ] : [] } - popover={[ + popover={popover.concat([ { name: 'Name', content: event.name, @@ -225,13 +249,15 @@ export default function EventBlocks({ periodToString(event.period), ].join(' '), }, - ]} + ])} + overlay={overlay} capture={capture} sizeInfo={sizeInfo} includeDetailsPopover={!dragging && includeDetailsPopover} includeContent={includeContent} canBeTabFocused={canBeTabFocused} onSelectMeeting={onSelectMeeting} + schedule={scheduleId} selectedMeeting={selectedMeeting} deviceHasHover={deviceHasHover} handleMouseDown={handleMouseDown} diff --git a/src/components/Feedback/stylesheet.scss b/src/components/Feedback/stylesheet.scss index 59f0ddec..18c1a17c 100644 --- a/src/components/Feedback/stylesheet.scss +++ b/src/components/Feedback/stylesheet.scss @@ -24,7 +24,7 @@ --feedback-outer-color: #{$theme-light-background}; --feedback-inner-color: #{$theme-light-card-background}; } - + background-color: var(--feedback-outer-color); // Include theme switch transition @@ -57,25 +57,25 @@ display: flex; flex-direction: column; } - + .text { margin-top: 10px; font-size: 16px; margin-bottom: 20px; } - - .FeedbackTitle { + + .FeedbackTitle { font-size: 24px; margin-bottom: 16px; margin-top: 0; } - + .FormButtons { display: flex; flex-direction: row; align-items: flex-start; justify-content: space-between; - + & div { display: inline-block; justify-content: space-around; @@ -87,10 +87,10 @@ transition-property: background-color; } } - + .FormButton { vertical-align: middle; - height:40px; + height: 40px; width: 40px; border-radius: 5px; &.active { @@ -106,12 +106,12 @@ align-items: flex-start; justify-content: space-between; } - + .score { font-size: 14px; - color: #808080; + color: $color-neutral; } - + .FeedbackTextArea { margin-top: 20px; border: none; @@ -131,10 +131,10 @@ transition-property: background-color; &::placeholder { - color: #808080; + color: $color-neutral; } } - + .SubmitButton { position: relative; width: 100px; @@ -148,7 +148,7 @@ margin-right: auto; color: white; } - + .CloseIcon { position: absolute; top: 0; diff --git a/src/components/HeaderActionBar/index.tsx b/src/components/HeaderActionBar/index.tsx index 7e4ca2ee..f7c4c57a 100644 --- a/src/components/HeaderActionBar/index.tsx +++ b/src/components/HeaderActionBar/index.tsx @@ -1,25 +1,24 @@ -import { faGithub } from '@fortawesome/free-brands-svg-icons'; import { faDownload, faCalendarAlt, faPaste, - faAdjust, faCaretDown, + faHandHoldingDollar, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { useCallback, useContext } from 'react'; +import React, { useCallback, useState } from 'react'; -import { Button } from '..'; +import { Button, InvitationModal } from '..'; import { LARGE_MOBILE_BREAKPOINT, LARGE_DESKTOP_BREAKPOINT, } from '../../constants'; -import { ThemeContext } from '../../contexts'; import useMedia from '../../hooks/useMedia'; import { AccountContextValue } from '../../contexts/account'; import { classes } from '../../utils/misc'; import { DropdownMenu, DropdownMenuAction } from '../Select'; import AccountDropdown from '../AccountDropdown'; +import ShareIcon from '../ShareIcon'; import './stylesheet.scss'; @@ -53,11 +52,9 @@ export default function HeaderActionBar({ onDownloadCalendar = (): void => undefined, enableDownloadCalendar = false, }: HeaderActionBarProps): React.ReactElement { - const [theme, setTheme] = useContext(ThemeContext); - const handleThemeChange = useCallback(() => { - const newTheme = theme === 'light' ? 'dark' : 'light'; - setTheme(newTheme); - }, [theme, setTheme]); + const [invitationOpen, setInvitationOpen] = useState(false); + + const hideInvitation = useCallback(() => setInvitationOpen(false), []); // Coalesce the export options into the props for a single const enableExport = @@ -99,6 +96,19 @@ export default function HeaderActionBar({ return (
    + +
    - +
    Export
    - - - +
    diff --git a/src/components/HeaderActionBar/stylesheet.scss b/src/components/HeaderActionBar/stylesheet.scss index f268cec9..c4581e1e 100644 --- a/src/components/HeaderActionBar/stylesheet.scss +++ b/src/components/HeaderActionBar/stylesheet.scss @@ -5,6 +5,14 @@ align-items: stretch; justify-content: flex-end; + .invite-button { + .circle { + margin: 4px 0px 0px 8px; + width: 8px; + color: #ff7337; + } + } + @media (max-width: $desktop-breakpoint) { flex: 1; margin-left: 0; diff --git a/src/components/HeaderDisplay/index.tsx b/src/components/HeaderDisplay/index.tsx index 864c2088..290df570 100644 --- a/src/components/HeaderDisplay/index.tsx +++ b/src/components/HeaderDisplay/index.tsx @@ -128,10 +128,10 @@ export default function HeaderDisplay({ )} {/* Left-aligned logo */} - +
    {/* Term selector */} {termsState.type === 'loaded' ? ( diff --git a/src/components/HeaderDisplay/stylesheet.scss b/src/components/HeaderDisplay/stylesheet.scss index 775598cd..9bebfdf8 100644 --- a/src/components/HeaderDisplay/stylesheet.scss +++ b/src/components/HeaderDisplay/stylesheet.scss @@ -12,6 +12,11 @@ font-weight: bold; white-space: pre; padding: 0 16px; + + display: flex; + justify-content: center; + align-items: center; + user-select: none; } .tabs { diff --git a/src/components/InformationModal/index.tsx b/src/components/InformationModal/index.tsx index 401b4bbf..4f6459a1 100644 --- a/src/components/InformationModal/index.tsx +++ b/src/components/InformationModal/index.tsx @@ -1,17 +1,21 @@ import React, { useEffect, useState } from 'react'; import useLocalStorageState from 'use-local-storage-state'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { classes } from '../../utils/misc'; import { DESKTOP_BREAKPOINT } from '../../constants'; import Modal from '../Modal'; import useScreenWidth from '../../hooks/useScreenWidth'; +import Button from '../Button'; import './stylesheet.scss'; // Key to mark when a user has already been shown the information modal. // Update this when updating the contents of the modal. -const MODAL_LOCAL_STORAGE_KEY = '2023-04-05-spr2023-new-features-announcement'; +const MODAL_LOCAL_STORAGE_KEY = '2024-04-01-spr2024-new-features-announcement'; const OUTDATED_LOCAL_STORAGE_KEY = [ + '2023-04-05-spr2023-new-features-announcement', '2021-10-27-spr2022-schedule-versions-account-sync', '2023-03-05-spr2023-oscar-migration', ]; @@ -22,11 +26,25 @@ const OUTDATED_LOCAL_STORAGE_KEY = [ * Additionally, make sure to change `MODAL_LOCAL_STORAGE_KEY` * with another unique value that has never been used before. */ -export function InformationModalContent(): React.ReactElement { + +export type InformationModalContentProps = { + setShow: (show: boolean) => void; +}; + +export function InformationModalContent({ + setShow, +}: InformationModalContentProps): React.ReactElement { return ( <> + GT Scheduler Logo @@ -38,36 +56,35 @@ export function InformationModalContent(): React.ReactElement { margin: '16px auto', }} > - New Feature: Recurring Events + New Feature: Compare Schedules -

    April 6, 2023

    +

    April 2, 2024

    Hello Yellow Jackets! We are excited to announce a new feature for GT Scheduler.

    - Use Recurring Events to block out meetings, work shifts, and any other - weekly events you may need to schedule your classes around. -
    -
    - Add, edit, and delete events in the Recurring Events tab, next to the - Courses tab. + Share your schedule with other students and they can share theirs + back. Then toggle "Compare Schedules" and click on the other + students' schedules to compare.

    - Drag and drop (web-only) events on the schedule view to adjust times. + However, to keep GT Scheduler and its amazing features, we need to + maintain our costs. Please consider donating to help keep GT Scheduler + running!

    Event Blocks
    Drag Drop
    @@ -104,10 +121,18 @@ export default function InformationModal(): React.ReactElement { className={classes('InformationModal', mobile && 'mobile')} show={show} onHide={(): void => setShow(false)} - buttons={[{ label: 'Got it!', onClick: (): void => setShow(false) }]} - width={800} + buttons={[ + { + label: 'Donate Today', + onClick: (): void => { + setShow(false); + window.open('https://donorbox.org/gt-scheduler'); + }, + }, + ]} + width={850} > - + ); } diff --git a/src/components/InformationModal/stylesheet.scss b/src/components/InformationModal/stylesheet.scss index 13adc8d9..833fc785 100644 --- a/src/components/InformationModal/stylesheet.scss +++ b/src/components/InformationModal/stylesheet.scss @@ -1,23 +1,38 @@ +@import '../../variables.scss'; + .InformationModal { - align-items: center; + .close-button { + width: 16px; + height: 16px; + position: absolute; + right: 12px; + top: 12px; + padding: 15px; + color: $color-neutral; + border-radius: 50px; + } .information-content { display: flex; + position: relative; width: 100%; align-items: center; justify-content: center; + margin-bottom: 20px; + padding-left: 15%; + padding-right: 15%; p { margin: 0px; - width: 270px; + width: 50%; } .information-images { display: flex; + overflow: hidden; + align-items: center; .information-image { width: auto; - height: 275px; + height: 25vh; margin-left: 16px; display: block; - border: #505050 3px solid; - border-radius: 12px; overflow: hidden; } } @@ -31,6 +46,7 @@ width: 100%; align-items: center; justify-content: center; + padding: 0; p { margin: auto; width: 80vw; @@ -38,16 +54,15 @@ } .information-images { display: flex; + justify-content: space-evenly; .information-spacer { - flex: 1; + flex: .5; } .information-image { width: auto; - height: 65vw; + height: 45vw; margin: 16px 0px 0px 0px; display: block; - border: #505050 3px solid; - border-radius: 12px; overflow: hidden; } } diff --git a/src/components/InvitationAcceptModal/InvitationAcceptModal.tsx b/src/components/InvitationAcceptModal/InvitationAcceptModal.tsx new file mode 100644 index 00000000..3db335ca --- /dev/null +++ b/src/components/InvitationAcceptModal/InvitationAcceptModal.tsx @@ -0,0 +1,328 @@ +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import React, { useContext, useEffect, useState, useMemo } from 'react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { FriendContext, ScheduleContext } from '../../contexts'; +import Button from '../Button'; +import Modal from '../Modal'; +import InvitationModal from '../InvitationModal'; +import LoginModal from '../LoginModal'; +import SuccesfulInvitationImage from '../SuccesfulInvitationImage'; + +import './stylesheet.scss'; + +export type InvitationAcceptModalProps = { + handleCompareSchedules: ( + compare?: boolean, + pinnedSchedules?: string[], + pinSelf?: boolean, + expanded?: boolean + ) => void; + setShareBackRemount: React.Dispatch>; +}; + +export default function InvitationAcceptModal({ + handleCompareSchedules, + setShareBackRemount, +}: InvitationAcceptModalProps): React.ReactElement { + const [modalOpen, setModalOpen] = useState(false); + const [invitationModalOpen, setInvitationModalOpen] = + useState(false); + const [loginModalOpen, setLoginModalOpen] = useState(false); + const [friendID, setFriendID] = useState(); + const [email, setEmail] = useState(); + + const [searchParams] = useSearchParams(); + + const navigate = useNavigate(); + + const [{ friends }] = useContext(FriendContext); + const [{ allFriends, allVersionNames }, { setTerm }] = + useContext(ScheduleContext); + + const schedulesShared = useMemo(() => { + return Object.keys(allFriends) + .map((version_id) => { + if ( + friendID && + allFriends[version_id] && + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + friendID in allFriends[version_id]! + ) { + const versionName = allVersionNames.filter( + (v) => v.id === version_id + ); + if (versionName.length > 0) { + return versionName[0]?.name; + } + } + return undefined; + }) + .filter((v) => v) as string[]; + }, [friendID, allFriends, allVersionNames]); + + const schedulesReceived = useMemo((): string[] | undefined => { + if (friendID) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return Object.keys(friends[friendID]!.versions) + .map((version_id): string | undefined => { + return friends[friendID]?.versions[version_id]?.name; + }) + .filter((name) => name) as string[]; + } + return undefined; + }, [friendID, friends]); + + const friendName = useMemo((): string | undefined => { + if (friendID) { + return friends[friendID]?.name; + } + return undefined; + }, [friendID, friends]); + + useEffect(() => { + if ( + !searchParams.get('inviteId') || + !searchParams.get('status') || + !searchParams.get('email') + ) { + setModalOpen(false); + return; + } + + const tempEmail: string | null = searchParams.get('email'); + + if (friends) { + Object.keys(friends).forEach((f_i) => { + if (friends[f_i] && friends[f_i]?.email === tempEmail) { + setFriendID(f_i); + } + }); + + setModalOpen(true); + } + + if ( + searchParams.get('status') === 'success' && + searchParams.get('term') !== null + ) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + setTerm(searchParams.get('term')!); + } + }, [searchParams, friends, setTerm]); + + useEffect(() => { + if (searchParams.get('email') !== null && searchParams.get('email')) { + setEmail(searchParams.get('email') ?? ''); + } + }, [searchParams]); + + const onHide = (): void => { + setModalOpen(!modalOpen); + navigate('/'); + handleCompareSchedules(true, undefined, undefined, true); + }; + + return ( + <> + { + setInvitationModalOpen(false); + }} + inputEmail={email} + /> + + { + setLoginModalOpen(false); + }} + /> + + { + onHide(); + setLoginModalOpen(true); + }, + }, + ] + : schedulesShared && + schedulesShared.length !== allVersionNames.length && + searchParams.get('status') === 'success' + ? [ + { + label: 'No', + onClick: onHide, + cancel: true, + }, + { + label: 'Share Back', + onClick: (): void => { + onHide(); + localStorage.setItem( + `share-back-invitation-${friendID ?? ''}`, + 'true' + ); + setShareBackRemount(1); + setInvitationModalOpen(true); + }, + }, + ] + : [] + } + > + + {searchParams.get('status') === 'success' ? ( + + ) : ( + + )} + + + ); +} + +type SuccessContentProps = { + email: string; + name: string; + schedulesReceived: string[]; + schedulesSent: string[]; +}; + +function SuccessContent({ + name, + email, + schedulesReceived, + schedulesSent, +}: SuccessContentProps): React.ReactElement { + return ( +
    +
    + You have successfully added a new schedule to your view! +
    + + + +
    + You will now be able to see {email}'s schedule! +
    +
    + Schedules {`${name}`} has shared with you: + {schedulesReceived && + schedulesReceived.map((version, i) => { + if (i !== schedulesReceived.length - 1) { + return ( + + {' '} + {version}, + + ); + } + return ( + + {' '} + {version} + + ); + })} + {schedulesReceived?.length === 0 ? ( + + None + + ) : null} +
    +
    + Schedules you have shared with {`${name}`}: + {schedulesSent && + schedulesSent.map((version, i) => { + if (i !== schedulesSent.length - 1) { + return ( + + {' '} + {version}, + + ); + } + return ( + + {' '} + {version} + + ); + })} + {schedulesSent?.length === 0 ? ( + + None + + ) : null} +
    +
    + ); +} + +type FailureContentProps = { + error: string; +}; + +function FailureContent({ error }: FailureContentProps): React.ReactElement { + return ( +
    + buzz +
    Failed to add new schedules
    +
    + {error === 'invalid-invite' + ? 'Invalid Invite' + : error === 'invite-expired' + ? 'Invite Expired' + : error === 'not-logged-in' + ? 'Not Logged In' + : error === 'already-accepted-all' + ? 'Schedules Already Accepted' + : "Something's wrong here.."} +
    +
    + {error === 'invalid-invite' ? ( + + The invite request is invalid, please ask the user for a new + invite. + + ) : error === 'invite-expired' ? ( + + The invite request has expired, please ask the user for a new + invite. + + ) : error === 'not-logged-in' ? ( + + Login and click on the invite link again to add your friend's + schedule to your view. + + ) : error === 'already-accepted-all' ? ( + You have already accepted these schedules from the user! + ) : ( + + An unknown error occurred on our end, please ask the user for a new + invite! + + )} +
    +
    + ); +} diff --git a/src/components/InvitationAcceptModal/stylesheet.scss b/src/components/InvitationAcceptModal/stylesheet.scss new file mode 100644 index 00000000..e9155851 --- /dev/null +++ b/src/components/InvitationAcceptModal/stylesheet.scss @@ -0,0 +1,55 @@ +@import '../../variables.scss'; + +.invitation-accept-modal-content { + .heading { + font-size: 24px; + color: var(--theme-fg); + text-align: center; + font-weight: 700; + } + + .modal-image { + width: 250px; + padding-top: 20px; + } + + .error-sub-heading { + color: var(--theme-fg); + font-size: 18px; + padding: 10px; + font-weight: 700; + } + + .error-message { + text-align: center; + } + + .sub-heading { + color: var(--theme-fg); + font-size: 14px; + padding-top: 15px; + text-align: center; + } + + .buzz-image { + width: 200px; + height: 200px; + margin: 10px; + } + + padding: 5% 10% 5% 10%; + display: flex; + flex-direction: column; + align-items: center; +} + +.remove-close-button { + width: 16px; + height: 16px; + position: absolute; + right: 12px; + top: 12px; + padding: 15px; + color: $color-neutral; + border-radius: 50px; +} diff --git a/src/components/InvitationModal/index.tsx b/src/components/InvitationModal/index.tsx new file mode 100644 index 00000000..e90d304b --- /dev/null +++ b/src/components/InvitationModal/index.tsx @@ -0,0 +1,726 @@ +import React, { + KeyboardEvent, + useCallback, + useContext, + useState, + useRef, + useMemo, +} from 'react'; +import { Tooltip as ReactTooltip } from 'react-tooltip'; +import { + faAngleDown, + faAngleUp, + faCheck, + faCircle, + faClose, + faLink, + faXmark, + faPaperPlane, + faXmarkCircle, + faCircleCheck, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import axios, { AxiosError, AxiosResponse } from 'axios'; +import copy from 'copy-to-clipboard'; + +import { ApiErrorResponse, FriendShareData } from '../../data/types'; +import { ScheduleContext } from '../../contexts'; +import { DESKTOP_BREAKPOINT, CLOUD_FUNCTION_BASE_URL } from '../../constants'; +import useScreenWidth from '../../hooks/useScreenWidth'; +import { classes } from '../../utils/misc'; +import Modal from '../Modal'; +import Button from '../Button'; +import { AccountContext, SignedIn } from '../../contexts/account'; +import { ErrorWithFields, softError } from '../../log'; +import Spinner from '../Spinner'; +import { ScheduleDeletionRequest } from '../../types'; +import useDeepCompareEffect from '../../hooks/useDeepCompareEffect'; + +import './stylesheet.scss'; + +/** + * Inner content of the invitation modal. + */ +export type InvitationModalContentProps = { + inputEmail?: string; +}; + +export function InvitationModalContent({ + inputEmail, +}: InvitationModalContentProps): React.ReactElement { + const [removeInvitationOpen, setRemoveInvitationOpen] = useState(false); + const [toRemoveInfo, setToRemoveInfo] = useState({ + version: { id: '', name: '' }, + friendId: '', + }); + const [otherSchedulesVisible, setOtherSchedulesVisible] = useState(false); + const [expirationDropdownVisible, setExpirationDropdownVisible] = + useState(false); + const [selectedExpiration, setSelectedExpiration] = useState('Never'); + + // All choices sent in seconds + const expirationChoices = useMemo( + (): Record => ({ + Never: 356 * 24 * 3600, + '1 week': 7 * 24 * 3600, + '1 day': 24 * 3600, + '1 hour': 3600, + }), + [] + ); + + const [{ currentVersion, term, allVersionNames, allFriends }] = + useContext(ScheduleContext); + const accountContext = useContext(AccountContext); + const mobile = !useScreenWidth(DESKTOP_BREAKPOINT); + + const input = useRef(null); + const [validMessage, setValidMessage] = useState(''); + const [validClassName, setValidClassName] = useState(''); + const [emailIcon, setEmailIcon] = useState('send'); + const [linkButtonClassName, setLinkButtonClassName] = useState(''); + const [linkLoading, setLinkLoading] = useState(false); + const [checkedSchedules, setCheckedSchedules] = useState([currentVersion]); + // const [invitationLink, setInvitationLink] = useState(''); + const [emailInput, setEmailInput] = useState(inputEmail ?? ''); + + const redirectURL = useMemo( + () => window.location.href.split('/#')[0] ?? '/', + [] + ); + + const handleChangeSearch = useCallback( + (e: React.ChangeEvent) => { + setEmailInput(e.target.value); + setValidMessage(''); + setValidClassName(''); + setEmailIcon('send'); + }, + [setEmailInput] + ); + + const sendInvitation = useCallback(async (): Promise => { + const IdToken = await (accountContext as SignedIn).getToken(); + const data = JSON.stringify({ + IDToken: IdToken, + term, + versions: checkedSchedules, + redirectURL, + friendEmail: input.current?.value, + }); + + return axios.post( + `${CLOUD_FUNCTION_BASE_URL}/createFriendInvitation`, + `data=${data}`, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + }, [accountContext, term, redirectURL, checkedSchedules]); + + // verify email with a regex and send invitation if valid + const verifyEmail = useCallback((): void => { + if (!input.current?.value) { + return; + } + + setEmailIcon('spinner'); + if (!/^\S+@\S+\.\S+$/.test(input.current.value)) { + setValidMessage('Invalid email, please try again!'); + setEmailIcon('send'); + return setValidClassName('invalid-email'); + } + const numNotAccepted = Object.entries(allFriends).reduce( + (acc, [versionId]) => { + if (!checkedSchedules.includes(versionId)) { + return acc; + } + const versionFriends = allFriends[versionId] as Record< + string, + FriendShareData + >; + + // if friend accepted, don't increment numNotAccepted + return Object.keys(versionFriends ?? {}).some((f) => { + return ( + versionFriends[f]?.email === input.current?.value && + (versionFriends[f]?.status === 'Accepted' || + versionFriends[f]?.status === 'Pending') + ); + }) + ? acc + : acc + 1; + }, + 0 + ); + + if (numNotAccepted === 0) { + setValidMessage('User has already been invited to selected schedules.'); + setEmailIcon('send'); + return setValidClassName('invalid-email'); + } + + sendInvitation() + .then(() => { + if (input.current) { + input.current.value = ''; + } + setValidMessage('Invite successfully sent!'); + setValidClassName('valid-email'); + setEmailInput(''); + setEmailIcon('checkmark'); + }) + .catch((err) => { + setValidClassName('invalid-email'); + setEmailIcon('send'); + const error = err as AxiosError; + if (error.response) { + const apiError = error.response.data as ApiErrorResponse; + setValidMessage(apiError.message); + return; + } + setValidMessage('Error sending invitation. Please try again later.'); + softError( + new ErrorWithFields({ + message: 'send email invitation failed', + source: err, + fields: { + user: (accountContext as SignedIn).id, + friendEmail: input.current?.value, + term, + versionIds: checkedSchedules, + }, + }) + ); + }); + }, [accountContext, sendInvitation, allFriends, checkedSchedules, term]); + + const getInvitationLink = useCallback(async (): Promise< + AxiosResponse<{ link: string }> + > => { + const IdToken = await (accountContext as SignedIn).getToken(); + const data = JSON.stringify({ + IDToken: IdToken, + term, + versions: checkedSchedules, + redirectURL, + validFor: expirationChoices[selectedExpiration], + }); + return axios.post( + `${CLOUD_FUNCTION_BASE_URL}/createFriendInvitationLink`, + `data=${data}`, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + }, [ + accountContext, + term, + redirectURL, + checkedSchedules, + expirationChoices, + selectedExpiration, + ]); + + const createLink = useCallback(async (): Promise => { + setLinkLoading(true); + setLinkButtonClassName(''); + await getInvitationLink() + .then((response) => { + copy(response.data.link); + }) + .catch((err) => { + setLinkButtonClassName('link-failure'); + softError( + new ErrorWithFields({ + message: 'invite link creation failed', + source: err, + fields: { + user: (accountContext as SignedIn).id, + term, + versionIds: checkedSchedules, + validFor: selectedExpiration, + }, + }) + ); + throw err; + }); + }, [ + accountContext, + term, + getInvitationLink, + checkedSchedules, + selectedExpiration, + ]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + verifyEmail(); + break; + default: + return; + } + e.preventDefault(); + }, + [verifyEmail] + ); + + // delete invitation or remove schedules from already accepted invitation + const handleDelete = useCallback( + async (versionId: string, friendId: string): Promise => { + const data = JSON.stringify({ + IDToken: await (accountContext as SignedIn).getToken(), + peerUserId: friendId, + term, + versions: [versionId], + owner: true, + } as ScheduleDeletionRequest); + axios + .post( + `${CLOUD_FUNCTION_BASE_URL}/deleteSharedSchedule`, + `data=${data}`, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ) + .catch((err) => { + throw err; + }); + }, + [accountContext, term] + ); + + function showRemoveInvitation( + version: { id: string; name: string }, + friendId: string + ): void { + setRemoveInvitationOpen(true); + setToRemoveInfo({ version, friendId }); + } + + // delete friend from record of friends and close modal + const hideRemoveInvitation = useCallback( + (confirm: boolean) => { + setRemoveInvitationOpen(false); + if (confirm) { + handleDelete(toRemoveInfo.version.id, toRemoveInfo.friendId).catch( + (err) => { + softError( + new ErrorWithFields({ + message: 'delete friend record from sender failed', + source: err, + fields: { + user: (accountContext as SignedIn).id, + friend: toRemoveInfo.friendId, + term, + version: toRemoveInfo.version.id, + }, + }) + ); + } + ); + } + }, + [toRemoveInfo, handleDelete, accountContext, term] + ); + + // show a fake loader when options change + useDeepCompareEffect(() => { + setLinkButtonClassName(''); + setLinkLoading(true); + setTimeout(() => { + setLinkLoading(false); + }, 200); + }, [checkedSchedules, selectedExpiration]); + + return ( +
    +
    +

    Share Schedule

    +

    + Enter an email associated with another user's GT-Scheduler + account & we'll send them an invite via email to import this + schedule into their view +

    +
    +
    + +
    + +
    +
    {validMessage}
    +
    + {allVersionNames.slice(0, 3).map((v) => ( + + ))} + {allVersionNames.length > 3 && ( +
    +
    + setOtherSchedulesVisible(!otherSchedulesVisible) + } + > +

    Other

    + +
    + {otherSchedulesVisible && ( +
    setOtherSchedulesVisible(false)} + /> + )} +
    + {otherSchedulesVisible && + allVersionNames + .slice(3) + .map((v) => ( + + ))} +
    +
    + )} +
    +
    +
    +
    + {allVersionNames.map((v) => { + return ( +
    +

    + Users Invited to View {v.name} +

    + {allFriends[v.id] && + Object.keys(allFriends[v.id] as Record) + .length !== 0 ? ( +
    + {Object.entries( + allFriends[v.id] as Record + ).map(([friendId, friend]) => ( +
    +
    +

    {friend.email}

    + + + Status: {friend.status} + +
    +
    + ))} +
    + ) : ( +
    + No friends have been invited +
    + )}{' '} +
    + ); + })} +
    +
    +
    +
    + +
    +
    + Link expires: +
    { + setExpirationDropdownVisible(!expirationDropdownVisible); + }} + > + {selectedExpiration} + +
    +
    + {expirationDropdownVisible && ( +
    setExpirationDropdownVisible(false)} + /> + )} +
    + {expirationDropdownVisible && + Object.keys(expirationChoices).map((exp) => ( + { + setSelectedExpiration(exp); + setExpirationDropdownVisible(false); + }} + > + {exp} + + ))} +
    +
    +
    +
    + + )[toRemoveInfo.friendId]?.email ?? '' + } + /> +
    + ); +} + +export type RemoveInvitationModalContentProps = { + versionName: string; + currentInvitee: string; +}; + +export function RemoveInvitationModalContent({ + versionName, + currentInvitee, +}: RemoveInvitationModalContentProps): React.ReactElement { + return ( +
    +
    +

    Remove Access

    +

    + Are you sure you want to remove the following user from having access + schedule: {versionName}? +

    +

    + User: {currentInvitee} +

    +

    + This user will only gain access to this schedule if you send them + another invitation +

    +
    +
    + ); +} + +export type InvitationModalProps = { + show: boolean; + onHide: () => void; + inputEmail?: string; +}; + +/** + * Component that can be used to show the invitaion modal. + */ +export default function InvitationModal({ + show, + onHide, + inputEmail, +}: InvitationModalProps): React.ReactElement { + return ( + + + + + ); +} + +export type RemoveInvitationModalProps = { + showRemove: boolean; + onHideRemove: (confirm: boolean) => void; + versionName: string; + currentInvitee: string; +}; + +function RemoveInvitationModal({ + showRemove, + onHideRemove, + versionName, + currentInvitee, +}: RemoveInvitationModalProps): React.ReactElement { + return ( + onHideRemove(false)} + buttons={[ + { label: 'Remove', onClick: () => onHideRemove(true), cancel: true }, + ]} + width={550} + > + + + + ); +} + +export type ShareScheduleCheckboxProps = { + checkedSchedules: string[]; + setCheckedSchedules: React.Dispatch>; + version: { id: string; name: string }; + isOther: boolean; +}; + +function ShareScheduleCheckbox({ + checkedSchedules, + setCheckedSchedules, + version, + isOther, +}: ShareScheduleCheckboxProps): React.ReactElement { + return ( +
    { + const newChecked = checkedSchedules; + if (!newChecked.includes(version.id)) { + newChecked.push(version.id); + } else if (newChecked.length > 1) { + newChecked.splice(newChecked.indexOf(version.id), 1); + } + setCheckedSchedules([...newChecked]); + }} + > + +

    {version.name}

    +
    + ); +} diff --git a/src/components/InvitationModal/stylesheet.scss b/src/components/InvitationModal/stylesheet.scss new file mode 100644 index 00000000..4e9d01d5 --- /dev/null +++ b/src/components/InvitationModal/stylesheet.scss @@ -0,0 +1,542 @@ +@import '../../variables'; + +.invitation-modal { + .modal__content { + padding: 0px; + } + + .remove-close-button { + width: 16px; + height: 16px; + position: absolute; + right: 12px; + top: 12px; + padding: 15px; + color: $color-neutral; + border-radius: 50px; + } + + .invitation-modal-content { + text-align: center; + overflow: hidden; + padding: 20px 40px; + background-color: var(--theme-bg); + color: var(--theme-fg); + + //Style for email input + .top-block { + display: flex; + flex-direction: column; + justify-content: center; + padding: 10px 0px 5px 0px; + + .modal-title { + font-size: 24px; + font-weight: 700; + margin-bottom: 10px; + } + + p { + font-size: 0.9em; + } + + .valid-email { + text-align: start; + color: #22b531; + font-size: 12px; + font-weight: bold; + padding: 3px 0px; + opacity: 1; + } + + .invalid-email { + text-align: start; + color: #ff2121; + font-size: 12px; + font-weight: bold; + padding-top: 3px; + opacity: 1; + } + + .email-input-block { + display: flex; + flex-direction: row; + justify-content: space-between; + padding-top: 5px; + align-items: center; + column-gap: 5px; + + .email-input { + display: flex; + flex-direction: column; + position: relative; + flex-grow: 1; + + .email:has(+ .invalid-email) { + border: 1px solid #ff2121; + } + } + + input[type='email'] { + padding: 5px; + line-height: 12px; + padding: 12px; + border-radius: 6px; + background-color: rgb(35, 35, 35); + border: 1px solid rgb(35, 35, 35); + outline: none; + + &:focus { + border-color: rgba(255, 255, 255, 0.5); + } + + @include light { + background-color: rgba($color-neutral, 0.3); + border: 1px solid rgba($color-neutral, 0.3); + + &:focus { + border-color: rgba(0, 0, 0, 0.5); + } + } + } + + .send-button { + width: 50px; + align-self: stretch; + font-size: 14px; + color: white; + border-radius: 6px; + background-color: #c56e5b; + border: none; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: #e2944b; + cursor: pointer; + } + } + + .disabled-send-button { + background-color: $color-neutral; + + &:hover { + background-color: $color-neutral; + cursor: default; + } + } + + .email-button-spinner { + background-color: #d89758; + + &:hover { + cursor: default; + } + } + + .email-button-checkmark { + background-color: #22b531; + + &:hover { + background-color: #22b531; + cursor: default; + } + + .email-button-check-icon { + width: 20px; + height: 20px; + } + } + } + + .share-schedule-checkboxes { + display: flex; + flex-direction: row; + align-items: center; + gap: 15px; + padding: 15px 0px 15px 2px; + + .other-schedules-button { + display: flex; + flex-direction: row; + align-items: center; + + &:hover { + cursor: pointer; + } + } + + .other-schedules-text { + text-decoration: underline; + margin-right: 5px; + margin-bottom: 0px; + } + + .other-schedules-list { + @include popup; + + margin: 0; + padding: 2px 0px; + z-index: 2; + position: absolute; + max-width: 150px; + overflow-x: auto; + overflow-y: auto; + max-height: 300px; + @include dark { + background-color: $theme-dark-background; + } + + @include light { + background-color: $theme-light-background; + } + } + } + } + + .divider { + height: 1.5px; + display: flex; + align-items: stretch !important; + border: none; + background-color: var(--theme-card-bg); + margin: 0px -40px; + } + + //Style for Invited Users + + .invited-users { + padding-top: 15px; + justify-content: center; + overflow-y: scroll; + height: 215px; + } + + .no-invited-users { + font-size: 14px; + padding: 10px 0px 25px 0px; + } + + .shared-emails { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + row-gap: 8px; + padding-bottom: 25px; + color: var(--theme-fg); + + .individual-shared-email { + @include card; + background-color: var(--theme-card-bg); + padding: 4px 8px; + margin: 4px 8px 4px 0px; + height: 100%; + display: flex; + flex-direction: row; + justify-content: center; + border-radius: 16px; + width: max-content; + max-width: 100%; + min-width: min-content; + font-size: 0.9em; + cursor: pointer; + + @include light { + background-color: rgba($color-neutral, 0.5); + opacity: 0.75; + } + + .email-text { + line-height: 100%; + } + } + } + + .email-and-status { + display: flex; + flex-direction: column; + height: 26px; + } + + .Pending { + border-color: rgba(205, 165, 24, 0.5); + border-style: solid; + border-width: 1.5px; + } + + .Accepted { + border-color: rgba(34, 181, 49, 0.5); + border-style: solid; + border-width: 1.5px; + } + + .status-tooltip { + background-color: rgba(0, 0, 0, 1); + border-radius: 4px; + } + + .button-remove { + width: 10px; + height: 10px; + background-color: transparent; + position: relative; + align-self: center; + + .circle { + width: 12px; + height: 12px; + padding: 10px; + position: absolute; + position: absolute; + color: white; + + @include light { + color: $color-neutral; + } + } + + .remove { + width: 10px; + height: 10px; + padding: 15px; + color: var(--theme-card-bg); + position: relative; + display: inline-flex; + } + + &:hover { + .circle { + color: $color-neutral; + + @include light { + color: var(--theme-fg); + } + } + + background-color: transparent; + } + } + } + + .modal-footer { + display: flex; + flex-direction: column; + padding-top: 24px; + + .link-options { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + .link-generated:hover { + cursor: pointer; + } + + .copy-link-button { + background: var(--theme-card-bg); + border-radius: 10px; + padding: 10px 15px; + border: none; + display: flex; + flex-direction: row; + align-items: center; + color: var(--theme-fg); + + .link-icon-container { + display: flex; + width: 20px; + margin-right: 5px; + } + + .copy-link-icon { + margin-right: 5px; + } + + &:hover { + @include dark { + background: $modal-foreground-color-light; + } + + @include light { + background: $modal-foreground-color-dark; + } + } + } + + .link-success { + color: #22b531; + } + + .link-failure { + color: #ff2121; + } + + .expiration { + position: relative; + + .expiration-display { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + + .current-expiration { + display: flex; + align-items: center; + justify-content: center; + gap: 5px; + background: var(--theme-card-bg); + margin-left: 5px; + border-radius: 30px; + padding: 4px 15px; + gap: 5px; + width: 100px; + + &:hover { + cursor: pointer; + + @include dark { + background: $modal-foreground-color-light; + } + + @include light { + background: $modal-foreground-color-dark; + } + } + } + } + + .expiration-select { + @include popup; + display: flex; + flex-direction: column; + width: 100px; + + margin: 0; + z-index: 2; + right: 0; + bottom: 100%; + position: absolute; + max-width: 150px; + overflow-x: auto; + background-color: var(--theme-bg); + + .expiration-option { + padding: 8px 15px; + + &:hover { + cursor: pointer; + background-color: var(--theme-card-bg); + } + } + } + } + } + } + + .mobile.invitation-modal-content { + .email-input-block { + flex-direction: column; + align-items: center; + .email-input { + max-width: 100%; + margin-bottom: 6px; + .email { + max-width: 100%; + } + } + } + } +} + +.intercept { + position: fixed; + z-index: 1; + top: 0; + bottom: 0; + left: 0; + right: 0; + cursor: default; +} + +.checkbox-and-label { + display: flex; + flex-direction: row; + align-items: center; + + &:hover { + cursor: pointer; + } + + .share-schedule-checkbox { + width: 12px; + height: 12px; + border: 2px solid; + border-radius: 2px; + border-color: var(--theme-fg); + margin-right: 8px; + padding: 1px; + color: var(--theme-bg); + } + + .schedule-checked { + background-color: var(--theme-fg); + } + + .checkbox-label { + font-size: 14px; + margin: 0px; + } +} + +.other-checkbox-and-label { + padding: 10px 15px; +} + +.modal-positioner { + .remove-invitation-modal { + overflow-y: hidden; + + .modal__content { + padding: 0px; + padding-top: 20px !important; + padding-bottom: 20px !important; + text-align: center; + + .remove-invitation-modal-content { + display: flex; + flex-direction: column; + align-items: center; + flex-wrap: wrap; + text-align: center; + padding-top: 15px; + padding-left: 50px; + padding-right: 50px; + height: 200px; + } + + .remove-close-button { + width: 16px; + height: 16px; + position: absolute; + right: 12px; + top: 12px; + padding: 15px; + color: $color-neutral; + border-radius: 50px; + } + } + + .modal__button--cancel { + background-color: #c56e5b !important; + + &:hover { + background-color: #e2944b !important; + } + } + } +} diff --git a/src/components/InviteBackLink/index.tsx b/src/components/InviteBackLink/index.tsx new file mode 100644 index 00000000..c5ab4af2 --- /dev/null +++ b/src/components/InviteBackLink/index.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import axios, { AxiosError, AxiosResponse } from 'axios'; + +import useFirebaseAuth from '../../data/hooks/useFirebaseAuth'; +import { CLOUD_FUNCTION_BASE_URL } from '../../constants'; +import { SignedIn } from '../../contexts'; +import Spinner from '../Spinner'; + +import './stylesheet.scss'; + +// eslint-disable-next-line no-shadow +enum LoadingState { + LOADING, + SUCCESS, + ERROR, +} + +type HandleInvitationResponse = { + email: string; + term: string; +}; + +interface ServerError extends AxiosError { + response: ServerErrorResponse; +} + +interface ServerErrorResponse extends AxiosResponse { + data: { + message: string; + }; +} + +const url = `${CLOUD_FUNCTION_BASE_URL}/handleFriendInvitation`; + +const handleInvite = async ( + inviteId: string | undefined, + token: string | void +): Promise => { + const data = JSON.stringify({ + inviteId, + token, + }); + const res = await axios.post(url, `data=${data}`, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + return res.data; +}; + +export default function InviteBackLink(): React.ReactElement { + const navigate = useNavigate(); + const location = useLocation(); + + const { id } = useParams(); + const [state, setState] = useState(LoadingState.LOADING); + + const redirectURL = useMemo( + () => + location.pathname.includes('/#') + ? location.pathname.split('/#')[0] ?? '/' + : '/', + [location] + ); + + const accountContext = useFirebaseAuth(); + + useEffect(() => { + const handleInviteAsync = async (): Promise< + HandleInvitationResponse | undefined + > => { + if (accountContext.type === 'loaded') { + const token = await (accountContext.result as SignedIn).getToken(); + return handleInvite(id, token); + } + return undefined; + }; + + const { type } = accountContext; + + if ( + type === 'loaded' && + accountContext.result.type === 'signedIn' && + redirectURL !== undefined + ) { + handleInviteAsync() + .then((resp) => { + setState(LoadingState.SUCCESS); + navigate( + `${redirectURL}?email=${resp?.email ?? ''}&term=${ + resp?.term ?? '' + }&status=success&inviteId=${id ?? ''}` + ); + }) + .catch((err: ServerError) => { + setState(LoadingState.ERROR); + navigate( + `${redirectURL}?email=none&status=${ + err.response?.data.message ?? '' + }&inviteId=${id ?? ''}` + ); + }); + } else if ( + type === 'loaded' && + accountContext.result.type !== 'signedIn' && + redirectURL !== undefined + ) { + navigate( + `${redirectURL}?email=none&status=not-logged-in&inviteId=${id ?? ''}` + ); + } + }, [id, navigate, redirectURL, accountContext.type]); // eslint-disable-line react-hooks/exhaustive-deps + + if (state === LoadingState.LOADING) { + return ( +
    + +

    Loading

    +
    friend schedule invite
    +
    + ); + } + + return ( +
    + {state === LoadingState.SUCCESS ? ( +

    Congratulations on Adding a New Schedule to your View!

    + ) : ( +

    We've Encountered an Error, Please Try Again

    + )} +

    You are being redirected to our main site, please wait...

    +

    + If you have not been redirected in 30 seconds, please click the button + below +

    + + + + Bits of Good Logo + +
    + ); +} diff --git a/src/components/InviteBackLink/stylesheet.scss b/src/components/InviteBackLink/stylesheet.scss new file mode 100644 index 00000000..22368c54 --- /dev/null +++ b/src/components/InviteBackLink/stylesheet.scss @@ -0,0 +1,79 @@ +@import '../../variables'; + +body { + background-color: $theme-dark-background; + color: $theme-dark-foreground; +} + +.Loading { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 100%; + + h4 { + font-size: 1.4rem; + margin-top: 12px; + margin-bottom: 8px; + } +} + +.EmailInviteConfirmation { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 20vh; + padding-left: 32px; + padding-right: 32px; + font-size: 24px; + height: 100%; + text-align: center; + + @media (max-width: 720px) { + padding-top: 60px; + } + + @media (max-width: 450px) { + padding-top: 60px; + font-size: 18px; + } + + @media (max-width: 300px) { + font-size: 16px; + } + + @media (max-height: 600px) { + padding-top: 60px; + } + + h1 { + font-size: 36px; + font-weight: 600; + + @media (max-width: 450px) { + font-size: 30px; + } + + @media (max-width: 300px) { + font-size: 24px; + } + } + + .footer { + position: absolute; + bottom: 38px; + } + + .continue-button { + background-color: #fe7c53; + color: white; + margin-top: 96px; + padding: 8px 24px; + border: none; + border-radius: 16px; + cursor: pointer; + font-size: 24px; + font-weight: 600; + } +} diff --git a/src/components/LoginModal/index.tsx b/src/components/LoginModal/index.tsx index 773755b3..e6fdcd58 100644 --- a/src/components/LoginModal/index.tsx +++ b/src/components/LoginModal/index.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import firebaseui from 'firebaseui'; import FirebaseAuth from 'react-firebaseui/FirebaseAuth'; +import { classes } from '../../utils/misc'; import Modal from '../Modal'; import { firebase, authProviders } from '../../data/firebase'; @@ -17,11 +18,15 @@ const uiConfig: firebaseui.auth.Config = { }, }; +export type LoginModalContentProps = { comparison: boolean }; + /** * Inner content of the login modal. * This utilizes Firebase UI to handle the authentication UI components. */ -export function LoginModalContent(): React.ReactElement { +export function LoginModalContent({ + comparison, +}: LoginModalContentProps): React.ReactElement { // Calculate the min height of the FirebaseUI element // so that it does not cause a large layout shift when initially loading. // The height is determined based on the number of auth providers, @@ -34,11 +39,20 @@ export function LoginModalContent(): React.ReactElement { return (
    -

    Sign in

    -

    - Sign in using one of the below identity providers to start syncing your - schedules across devices. -

    + {comparison ? ( +

    + You must sign in to use the Compare + Schedule Feature! +

    + ) : ( +

    Sign in

    + )} +
    +

    + Sign in using one of the below identity providers to start syncing + your schedules across devices. +

    +
    void; + comparison?: boolean; }; /** @@ -62,6 +77,7 @@ export type LoginModalProps = { export default function LoginModal({ show, onHide, + comparison = false, }: LoginModalProps): React.ReactElement { // If the modal is open, // attach a listener for the authentication state @@ -87,7 +103,7 @@ export default function LoginModal({ { label: 'Cancel', onClick: (): void => onHide(), cancel: true }, ]} > - + ); } diff --git a/src/components/LoginModal/stylesheet.scss b/src/components/LoginModal/stylesheet.scss index 741366c8..b18a08d9 100644 --- a/src/components/LoginModal/stylesheet.scss +++ b/src/components/LoginModal/stylesheet.scss @@ -62,4 +62,30 @@ text-align: center; margin-bottom: 28px; } + + .underline { + text-decoration: underline; + font-weight: 700; + color: #FFFFFF; + } + + .compare-text { + text-align: center; + font-size: 25px; + font-style: normal; + font-weight: 600; + line-height: normal; + padding: 10px 1px 0px 1px; + color: #FFFFFF; + } + + .compare-subtext { + width: 330px; + margin: 0 auto; + padding-bottom: 15px; + font-size: 15px; + font-weight: 400; + color: #FFFFFF; + + } } diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index b5c4b27f..43f589c2 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -21,6 +21,7 @@ export interface ModalButtonProps { export type ModalProps = { children?: React.ReactNode; + buttonPrompt?: string; buttons?: ModalButtonProps[]; show: boolean; onHide: () => void; @@ -58,6 +59,7 @@ type TransitionProps = { */ export default function Modal({ children, + buttonPrompt, buttons = [], show, onHide, @@ -110,6 +112,7 @@ export default function Modal({ checkboxContent && 'has-checkbox' )} > + {buttonPrompt &&
    {buttonPrompt}
    } {checkboxContent && (
    {checkboxContent}

    )} - {buttons.map((props, i) => ( - - ))} +
    + {buttons.map((props, i) => ( + + ))} +
    )}
    diff --git a/src/components/Modal/stylesheet.scss b/src/components/Modal/stylesheet.scss index 7e18e520..4363a2e2 100644 --- a/src/components/Modal/stylesheet.scss +++ b/src/components/Modal/stylesheet.scss @@ -187,6 +187,7 @@ text-align: center; display: flex; + flex-direction: column; align-items: center; justify-content: center; gap: 16px; @@ -232,6 +233,11 @@ font-size: .95em; } } + + .button_container { + display: flex; + column-gap: 20px; + } } &__button { diff --git a/src/components/RouterComponent/index.tsx b/src/components/RouterComponent/index.tsx new file mode 100644 index 00000000..ca55ed14 --- /dev/null +++ b/src/components/RouterComponent/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Routes, Route, Navigate, HashRouter } from 'react-router-dom'; + +import App from '../App'; +import InviteBackLink from '../InviteBackLink'; + +export default function RouterComponent(): React.ReactElement { + return ( + + + } /> + } /> + + } /> + } /> + + } /> + + + ); +} diff --git a/src/components/Scheduler/index.tsx b/src/components/Scheduler/index.tsx index 86012251..cc20c0bf 100644 --- a/src/components/Scheduler/index.tsx +++ b/src/components/Scheduler/index.tsx @@ -1,9 +1,20 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import { classes } from '../../utils/misc'; -import { Button, Calendar, CombinationContainer, CourseContainer } from '..'; -import { OverlayCrnsContext, OverlayCrnsContextValue } from '../../contexts'; +import { + Button, + Calendar, + CombinationContainer, + ComparisonPanel, + CourseContainer, +} from '..'; +import { + OverlayCrnsContext, + OverlayCrnsContextValue, + ScheduleContext, +} from '../../contexts'; import { DESKTOP_BREAKPOINT } from '../../constants'; +import useCompareStateFromStorage from '../../data/hooks/useCompareStateFromStorage'; import useScreenWidth from '../../hooks/useScreenWidth'; /** @@ -24,6 +35,28 @@ export default function Scheduler(): React.ReactElement { [overlayCrns, setOverlayCrns] ); + const [{ currentVersion }] = useContext(ScheduleContext); + + const { compare, pinned, pinSelf, expanded, setCompareState } = + useCompareStateFromStorage({ pinDefault: [currentVersion] }); + const [overlaySchedules, setOverlaySchedules] = useState([]); + + const handleCompareSchedules = useCallback( + ( + newCompare?: boolean, + newPinnedSchedules?: string[], + newPinSelf?: boolean, + newExpanded?: boolean, + newOverlaySchedules?: string[] + ) => { + setCompareState(newCompare, newPinnedSchedules, newPinSelf, newExpanded); + if (newOverlaySchedules !== undefined) { + setOverlaySchedules(newOverlaySchedules); + } + }, + [setCompareState, setOverlaySchedules] + ); + return ( <> {mobile && ( @@ -42,12 +75,27 @@ export default function Scheduler(): React.ReactElement {
    {(!mobile || tabIndex === 0) && } - {(!mobile || tabIndex === 1) && } + {mobile && tabIndex === 1 && } {(!mobile || tabIndex === 2) && (
    - +
    )} + {(!mobile || tabIndex === 3) && ( + + )}
    diff --git a/src/components/SectionBlocks/index.tsx b/src/components/SectionBlocks/index.tsx index 64ba040d..f4ee956a 100644 --- a/src/components/SectionBlocks/index.tsx +++ b/src/components/SectionBlocks/index.tsx @@ -18,6 +18,7 @@ export interface SectionBlockPosition extends TimeBlockPosition { export type SectionBlocksProps = { className?: string; crn: string; + schedule?: string; overlay?: boolean; capture: boolean; includeDetailsPopover: boolean; @@ -34,6 +35,7 @@ export type SectionBlocksProps = { export default function SectionBlocks({ className, crn, + schedule, overlay = false, capture, sizeInfo, @@ -57,9 +59,9 @@ export default function SectionBlocks({ return ( day !== 'S' && day !== 'U')} @@ -122,6 +124,7 @@ export default function SectionBlocks({ deviceHasHover={deviceHasHover} selectedMeeting={selectedMeeting} onSelectMeeting={onSelectMeeting} + schedule={schedule} /> ); })} diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index 3c16eebb..2fe18240 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -442,19 +442,21 @@ type AutoFocusInputProps = { onChange?: (e: React.ChangeEvent) => void; placeholder?: string; onKeyDown?: (e: React.KeyboardEvent) => void; + onBlur?: (e: React.FocusEvent) => void; }; /** * Simple wrapper around `` * that automatically focuses its contents when it is first mounted */ -function AutoFocusInput({ +export function AutoFocusInput({ className, style, value, onChange, placeholder, onKeyDown, + onBlur, }: AutoFocusInputProps): React.ReactElement { const inputRef = useRef(null); useEffect(() => { @@ -473,6 +475,7 @@ function AutoFocusInput({ onKeyDown={onKeyDown} ref={inputRef} type="text" + onBlur={onBlur} /> ); } diff --git a/src/components/ShareIcon/index.tsx b/src/components/ShareIcon/index.tsx new file mode 100644 index 00000000..04199712 --- /dev/null +++ b/src/components/ShareIcon/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { classes } from '../../utils/misc'; + +import './stylesheet.scss'; + +export interface ShareIconProps { + className: string; +} + +export default function ShareIcon({ className }: ShareIconProps): JSX.Element { + return ( + + + + ); +} diff --git a/src/components/ShareIcon/stylesheet.scss b/src/components/ShareIcon/stylesheet.scss new file mode 100644 index 00000000..1c78ca48 --- /dev/null +++ b/src/components/ShareIcon/stylesheet.scss @@ -0,0 +1,14 @@ +@import "../../variables"; + +.share-icon { + transition-duration: $theme-switch-transition-duration; + transition-property: fill; + + @include light { + fill: $theme-light-foreground; + } + + @include dark { + fill: $theme-dark-foreground; + } +} \ No newline at end of file diff --git a/src/components/SuccesfulInvitationImage/index.tsx b/src/components/SuccesfulInvitationImage/index.tsx new file mode 100644 index 00000000..c4994b53 --- /dev/null +++ b/src/components/SuccesfulInvitationImage/index.tsx @@ -0,0 +1,144 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export default function SuccesfulInvitationImage({ + className, +}: Props): React.ReactElement { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/TimeBlocks/index.tsx b/src/components/TimeBlocks/index.tsx index bebc2571..df3509ed 100644 --- a/src/components/TimeBlocks/index.tsx +++ b/src/components/TimeBlocks/index.tsx @@ -43,6 +43,7 @@ export type TimeBlocksProps = { includeContent: boolean; sizeInfo: SizeInfo; canBeTabFocused?: boolean; + schedule?: string; /** * Passing through this prop to skip subscribing to a media query per * TimeBlocks component instance: @@ -86,10 +87,11 @@ export default function TimeBlocks({ deviceHasHover = true, selectedMeeting, onSelectMeeting, + schedule, handleMouseDown, }: TimeBlocksProps): React.ReactElement | null { const [{ colorMap }] = useContext(ScheduleContext); - const color = colorMap[id]; + const color = colorMap[schedule ?? id]; const sizeInfoKey = makeSizeInfoKey(period); return ( @@ -252,17 +254,22 @@ function MeetingDayBlock({ {includeContent && (
    - {contentHeader.map((content) => { + {contentHeader.map((content, i) => { return ( - + {content.content}  ); })}
    - {contentBody.map((content) => { + {contentBody.map((content, i) => { return ( - {content.content} + + {content.content} + ); })}
    @@ -316,9 +323,9 @@ function DetailsPopoverContent({ return ( - {popover.map((popoverInfo) => { + {popover.map((popoverInfo, i) => { return popoverInfo.content ? ( - + diff --git a/src/components/TimeBlocks/stylesheet.scss b/src/components/TimeBlocks/stylesheet.scss index 3b015d23..a89c2a4d 100644 --- a/src/components/TimeBlocks/stylesheet.scss +++ b/src/components/TimeBlocks/stylesheet.scss @@ -39,13 +39,15 @@ border: none; text-align: left; padding: 0; + border-top: 1px solid $color-border; + border-left: 1px solid $color-border; &:focus { // Remove the default focus style, since selecting the meeting // adds its own focus style: outline: none; } - + // Add a box-shadow border to the meeting block when it is highlighted &::after { content: ''; @@ -60,7 +62,7 @@ border-radius: 2px; transition: opacity 0.2s ease-in-out; } - + &--selected { &::after { opacity: 0.5; @@ -100,7 +102,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - font-size: .72em; + font-size: 0.72em; } .ids { @@ -109,7 +111,7 @@ overflow: hidden; span { - font-size: .8em; + font-size: 0.8em; } } } diff --git a/src/components/index.ts b/src/components/index.ts index 47d8b7ab..d3eac0c4 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -24,6 +24,13 @@ export { default as Tab } from './Tab'; export { default as TimeBlocks } from './TimeBlocks'; export { default as SectionBlocks } from './SectionBlocks'; export { default as EventBlocks } from './EventBlocks'; +export { default as CompareBlocks } from './CompareBlocks'; export { default as Attribution } from './Attribution'; export { default as Event } from './Event'; export { default as CourseNavMenu } from './CourseNavMenu'; +export { default as ComparisonContainer } from './ComparisonContainer'; +export { default as ComparisonPanel } from './ComparisonPanel'; +export { default as InviteBackLink } from './InviteBackLink'; +export { default as RouterComponent } from './RouterComponent'; +export { default as InvitationModal } from './InvitationModal'; +export { default as ShareIcon } from './ShareIcon'; diff --git a/src/contexts/account.ts b/src/contexts/account.ts index a98beed7..5bccfd99 100644 --- a/src/contexts/account.ts +++ b/src/contexts/account.ts @@ -7,6 +7,7 @@ export type SignedOut = { export type SignedIn = { type: 'signedIn'; signOut: () => void; + getToken: () => Promise; name: string | null; provider: string | null; email: string | null; diff --git a/src/contexts/friend.ts b/src/contexts/friend.ts new file mode 100644 index 00000000..389dbb9b --- /dev/null +++ b/src/contexts/friend.ts @@ -0,0 +1,52 @@ +import { Immutable, Draft } from 'immer'; +import React from 'react'; + +import { + defaultFriendScheduleData, + FriendInfo, + FriendScheduleData, + FriendTermData, +} from '../data/types'; +import { ErrorWithFields } from '../log'; + +export type FriendContextData = Immutable<{ friends: FriendScheduleData }>; +export type FriendContextSetters = { + updateFriendTermData: ( + applyDraft: ( + draft: Draft + ) => void | Immutable + ) => void; + updateFriendInfo: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; + renameFriend: (id: string, newName: string) => void; +}; + +export type FriendContextValue = [FriendContextData, FriendContextSetters]; + +export const FriendContext = React.createContext([ + { + friends: defaultFriendScheduleData, + }, + { + updateFriendTermData: (): void => { + throw new ErrorWithFields({ + message: 'empty FriendContext.updateFriendTermData value being used', + }); + }, + updateFriendInfo: (): void => { + throw new ErrorWithFields({ + message: 'empty FriendContext.updateFriendInfo value being used', + }); + }, + renameFriend: (id: string, newName: string): void => { + throw new ErrorWithFields({ + message: 'empty FriendContext.renameFriend value being used', + fields: { + id, + newName, + }, + }); + }, + }, +]); diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 942ff79f..c9e95a9d 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,4 +1,6 @@ +export * from './friend'; export * from './theme'; export * from './terms'; export * from './schedule'; export * from './overlayCrns'; +export * from './account'; diff --git a/src/contexts/schedule.ts b/src/contexts/schedule.ts index 12b0a8d5..40a69827 100644 --- a/src/contexts/schedule.ts +++ b/src/contexts/schedule.ts @@ -3,19 +3,28 @@ import { Draft, Immutable } from 'immer'; import { Oscar } from '../data/beans'; import { EMPTY_OSCAR } from '../data/beans/Oscar'; -import { defaultSchedule, Schedule } from '../data/types'; +import { + defaultSchedule, + FriendShareData, + Schedule, + TermScheduleData, +} from '../data/types'; import { ErrorWithFields } from '../log'; type ExtraData = { term: string; currentVersion: string; + currentFriends: Record; + allFriends: Record>; allVersionNames: { id: string; name: string }[]; // `oscar` is included below as a separate type }; export type ScheduleContextData = Immutable & // `Oscar` can't go into `Immutable`, so we place it separately - Immutable & { readonly oscar: Oscar }; + Immutable & { + readonly oscar: Oscar; + } & Immutable; export type ScheduleContextSetters = { setTerm: (next: string) => void; @@ -28,6 +37,7 @@ export type ScheduleContextSetters = { deleteVersion: (id: string) => void; renameVersion: (id: string, newName: string) => void; cloneVersion: (id: string, newName: string) => void; + deleteFriendRecord: (versionId: string, friendId: string) => void; }; export type ScheduleContextValue = [ ScheduleContextData, @@ -37,9 +47,12 @@ export const ScheduleContext = React.createContext([ { term: '', currentVersion: '', + currentFriends: {}, allVersionNames: [], + allFriends: {}, oscar: EMPTY_OSCAR, ...defaultSchedule, + versions: {}, }, { setTerm: (next: string): void => { @@ -71,6 +84,15 @@ export const ScheduleContext = React.createContext([ }, }); }, + deleteFriendRecord: (versionId: string, friendId: string): void => { + throw new ErrorWithFields({ + message: 'empty ScheduleContext.deleteFriendRecord value being used', + fields: { + versionId, + friendId, + }, + }); + }, addNewVersion: (name: string, select?: boolean): string => { throw new ErrorWithFields({ message: 'empty ScheduleContext.addNewVersion value being used', diff --git a/src/data/beans/Oscar.ts b/src/data/beans/Oscar.ts index b9cc3776..c6e62500 100644 --- a/src/data/beans/Oscar.ts +++ b/src/data/beans/Oscar.ts @@ -175,12 +175,35 @@ export default class Oscar { }); this.sortingOptions = [ - new SortingOption('Most Compact', (combination) => { + new SortingOption('Most Compact', (combination, events) => { const { startMap, endMap } = combination; + + const eventStartMap = new Map(); + const eventEndMap = new Map(); + events.forEach((event) => { + const { start, end } = event.period; + for (const day of event.days) { + if (!eventStartMap.has(day)) { + eventStartMap.set(day, start); + } + eventStartMap.set( + day, + Math.min(start, eventStartMap.get(day) ?? Infinity) + ); + + if (!eventEndMap.has(day)) { + eventEndMap.set(day, end); + } + eventEndMap.set(day, Math.max(end, eventEndMap.get(day) ?? -1)); + } + }); const diffs = Object.keys(startMap).map((day) => { - const end = endMap[day]; - const start = startMap[day]; + let end = endMap[day]; + let start = startMap[day]; if (end == null || start == null) return 0; + end = Math.max(end, eventEndMap.get(day) ?? -1); + start = Math.min(start, eventStartMap.get(day) ?? Infinity); + return end - start; }); const sum = diffs.reduce((tot, min) => tot + min, 0); @@ -314,7 +337,8 @@ export default class Oscar { sortCombinations( combinations: Combination[], - sortingOptionIndex: number + sortingOptionIndex: number, + events: Immutable ): Combination[] { const sortingOption = this.sortingOptions[sortingOptionIndex]; if (sortingOption === undefined) { @@ -331,7 +355,7 @@ export default class Oscar { return combinations .map((combination) => ({ ...combination, - factor: sortingOption.calculateFactor(combination), + factor: sortingOption.calculateFactor(combination, events), })) .sort((a, b) => a.factor - b.factor); } diff --git a/src/data/beans/SortingOption.ts b/src/data/beans/SortingOption.ts index 1ca19371..b61c6b7a 100644 --- a/src/data/beans/SortingOption.ts +++ b/src/data/beans/SortingOption.ts @@ -1,11 +1,16 @@ -import { Combination } from '../../types'; +import { Immutable } from 'immer'; + +import { Combination, Event } from '../../types'; export default class SortingOption { label: string; - calculateFactor: (combo: Combination) => number; + calculateFactor: (combo: Combination, events: Immutable) => number; - constructor(label: string, calculateFactor: (combo: Combination) => number) { + constructor( + label: string, + calculateFactor: (combo: Combination, events: Immutable) => number + ) { this.label = label; this.calculateFactor = calculateFactor; } diff --git a/src/data/firebase.ts b/src/data/firebase.ts index 18fb21ab..f4d3a2ef 100644 --- a/src/data/firebase.ts +++ b/src/data/firebase.ts @@ -3,7 +3,7 @@ import 'firebase/auth'; import 'firebase/firestore'; import { ErrorWithFields, softError } from '../log'; -import { AnyScheduleData } from './types'; +import { AnyScheduleData, FriendData } from './types'; // This data is not secret; it is included in the application bundle. // Supply these environment variables when developing locally. @@ -18,6 +18,7 @@ export const firebaseConfig = { }; const SCHEDULE_COLLECTION = 'schedules'; +const FRIEND_COLLECTION = 'friends'; /** * Whether Firebase authentication is enabled in this environment. @@ -32,8 +33,12 @@ let db: firebase.firestore.Firestore = null as unknown as firebase.firestore.Firestore; type SchedulesCollection = firebase.firestore.CollectionReference; +type FriendsCollection = firebase.firestore.CollectionReference; let schedulesCollection: SchedulesCollection = null as unknown as SchedulesCollection; + +let friendsCollection: FriendsCollection = null as unknown as FriendsCollection; + /* eslint-enable import/no-mutable-exports */ if (isAuthEnabled) { const app = firebase.initializeApp(firebaseConfig); @@ -44,6 +49,8 @@ if (isAuthEnabled) { SCHEDULE_COLLECTION ) as SchedulesCollection; + friendsCollection = db.collection(FRIEND_COLLECTION) as FriendsCollection; + auth.setPersistence(firebase.auth.Auth.Persistence.LOCAL).catch((err) => { softError( new ErrorWithFields({ @@ -54,7 +61,7 @@ if (isAuthEnabled) { }); } -export { auth, db, schedulesCollection }; +export { auth, db, schedulesCollection, friendsCollection }; export { firebase }; // Configure the enabled auth providers that firebase UI displays as options diff --git a/src/data/hooks/useCompareStateFromStorage.ts b/src/data/hooks/useCompareStateFromStorage.ts new file mode 100644 index 00000000..9dc6b6b1 --- /dev/null +++ b/src/data/hooks/useCompareStateFromStorage.ts @@ -0,0 +1,99 @@ +import { useCallback } from 'react'; +import useLocalStorageState from 'use-local-storage-state'; + +type HookResult = { + compare: boolean; + pinned: string[]; + pinSelf: boolean; + expanded: boolean; + setCompareState: ( + newCompare: boolean | undefined, + newPinned: string[] | undefined, + newPinSelf: boolean | undefined, + newExpanded: boolean | undefined + ) => void; +}; + +type Props = { + compareDefault?: boolean; + pinDefault?: string[]; + pinSelfDefault?: boolean; + expandedDefault?: boolean; +}; + +/** + * Gets the current UI state from local storage. + * Do not call this function in a non-root component; + * it should only be called once in a root component (i.e. ). + * Moreover, unlike the local storage version of the app data, + * this **does not** sync between tabs. + * This is deliberate, as it allows opening up multiple tabs + * with different schedules if desired, + * but still have the app resume to the last viewed schedule when opened again. + */ +export default function useCompareStateFromStorage({ + compareDefault, + pinDefault, + pinSelfDefault, + expandedDefault, +}: Props): HookResult { + const [compare, setCompare] = useLocalStorageState( + 'compare-panel-state-compareValue', + { + defaultValue: compareDefault ?? false, + storageSync: false, + } + ); + const [pinned, setPinned] = useLocalStorageState( + 'compare-panel-state-pinnedSchedules', + { + defaultValue: pinDefault ?? [], + storageSync: false, + } + ); + const [pinSelf, setPinSelf] = useLocalStorageState( + 'compare-panel-state-pinSelfValue', + { + defaultValue: pinSelfDefault ?? true, + storageSync: false, + } + ); + const [expanded, setExpanded] = useLocalStorageState( + 'compare-panel-state-expandedValue', + { + defaultValue: expandedDefault ?? true, + storageSync: false, + } + ); + + const setCompareState = useCallback( + ( + newCompare?: boolean, + newPinnedSchedules?: string[], + newPinSelf?: boolean, + newExpanded?: boolean + ) => { + if (newCompare !== undefined) { + setCompare(newCompare); + } + if (newPinnedSchedules !== undefined) { + setPinned(newPinnedSchedules); + } + if (newPinSelf !== undefined) { + setPinSelf(newPinSelf); + } + if (newExpanded !== undefined) { + setExpanded(newExpanded); + } + }, + [setCompare, setPinned, setPinSelf, setExpanded] + ); + + return { + compare, + pinned, + pinSelf, + expanded, + setCompareState, + }; +} diff --git a/src/data/hooks/useEnsureValidTerm.ts b/src/data/hooks/useEnsureValidTerm.ts index 74e0502f..54196b65 100644 --- a/src/data/hooks/useEnsureValidTerm.ts +++ b/src/data/hooks/useEnsureValidTerm.ts @@ -22,7 +22,9 @@ export default function useEnsureValidTerm({ }): LoadingState { // Set the term to be the first one if it is unset or no longer valid. useEffect(() => { - const mostRecentTerm = terms[0].term; + const mostRecentTerm = + terms.find((term) => term.finalized === true)?.term ?? terms[0].term; + const correctedTerm = !isValidTerm(currentTermRaw, terms) ? mostRecentTerm : currentTermRaw; diff --git a/src/data/hooks/useExtractFriendInfo.ts b/src/data/hooks/useExtractFriendInfo.ts new file mode 100644 index 00000000..b07f2ee5 --- /dev/null +++ b/src/data/hooks/useExtractFriendInfo.ts @@ -0,0 +1,138 @@ +import produce, { Immutable, Draft, castDraft, castImmutable } from 'immer'; +import { useEffect, useCallback, useMemo } from 'react'; + +import { LoadingState } from '../../types'; +import { + FriendData, + defaultFriendInfo, + FriendInfo, + RawFriendScheduleData, + FriendScheduleData, +} from '../types'; +import { ErrorWithFields, softError } from '../../log'; + +/** + * Gets the current term friend info based on the current term. + * If the term friend info for the current term doesn't exist, + * then this hook also initializes it to an empty value. + */ +export default function useExtractFriendInfo({ + rawFriendScheduleData, + friendInfo, + updateFriendData, +}: { + rawFriendScheduleData: RawFriendScheduleData; + friendInfo: Immutable; + updateFriendData: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; +}): LoadingState<{ + friendScheduleData: Immutable; + updateFriendInfo: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; +}> { + // Ensure that there is a valid term friend info object for the term + useEffect(() => { + if (friendInfo === undefined) { + updateFriendData((draft) => { + draft.info = castDraft({}); + }); + return; + } + + for (const friendId of Object.keys(rawFriendScheduleData)) { + const currentFriendInfo = friendInfo[friendId]; + const correctedFriendInfo = + currentFriendInfo === undefined || + currentFriendInfo.name === undefined || + currentFriendInfo.email === undefined + ? defaultFriendInfo + : currentFriendInfo; + if (correctedFriendInfo !== currentFriendInfo) { + updateFriendData((draft) => { + draft.info[friendId] = castDraft(correctedFriendInfo); + }); + return; + } + } + }, [rawFriendScheduleData, friendInfo, updateFriendData]); + + // Create a nested update callback for just the friend info. + const updateFriendInfo = useCallback( + ( + applyDraft: (draft: Draft) => void | Immutable + ): void => { + updateFriendData((draft) => { + const currentFriendInfoDraft = draft.info ?? null; + if (currentFriendInfoDraft === null) { + softError( + new ErrorWithFields({ + message: + 'updateFriendInfo called with invalid info field; ignoring', + fields: { + currentFriendInfo: null, + }, + }) + ); + return; + } + + draft.info = produce(currentFriendInfoDraft, (subDraft) => + castDraft(applyDraft(subDraft)) + ); + }); + }, + [updateFriendData] + ); + + const friendScheduleData = + useMemo | null>(() => { + const temp: FriendScheduleData = {}; + if (friendInfo === undefined) return null; + + for (const friendId of Object.keys(rawFriendScheduleData)) { + const currentFriendInfo = friendInfo[friendId]; + if ( + currentFriendInfo === undefined || + currentFriendInfo.email === undefined || + currentFriendInfo.name === undefined + ) + return null; + + const rawFriendScheduleDatum = rawFriendScheduleData[friendId]; + if ( + rawFriendScheduleDatum === undefined || + rawFriendScheduleDatum.versions === undefined + ) { + softError( + new ErrorWithFields({ + message: 'an error occurred when accessing friend schedule data', + fields: { + friendId, + }, + }) + ); + } else { + temp[friendId] = { + ...currentFriendInfo, + ...rawFriendScheduleDatum, + }; + } + } + + return castImmutable(temp); + }, [friendInfo, rawFriendScheduleData]); + + if (friendScheduleData === null) { + return { type: 'loading' }; + } + + return { + type: 'loaded', + result: { + friendScheduleData, + updateFriendInfo, + }, + }; +} diff --git a/src/data/hooks/useExtractFriendTermData.ts b/src/data/hooks/useExtractFriendTermData.ts new file mode 100644 index 00000000..087a46f0 --- /dev/null +++ b/src/data/hooks/useExtractFriendTermData.ts @@ -0,0 +1,110 @@ +import produce, { Immutable, Draft, castDraft } from 'immer'; +import { useEffect, useCallback } from 'react'; + +import { LoadingState } from '../../types'; +import { + FriendData, + defaultFriendTermData, + FriendTermData, + FriendIds, +} from '../types'; +import { ErrorWithFields, softError } from '../../log'; + +/** + * Gets the current term friend data based on the current term. + * If the term friend data for the current term doesn't exist, + * then this hook also initializes it to an empty value. + */ +export default function useExtractFriendTermData({ + currentTerm, + rawFriendData, + updateFriendData, +}: { + currentTerm: string; + rawFriendData: Immutable; + updateFriendData: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; +}): LoadingState<{ + termFriendData: Immutable; + updateFriendTermData: ( + applyDraft: ( + draft: Draft + ) => void | Immutable + ) => void; +}> { + // Ensure that there is a valid term friend data object for the term + useEffect(() => { + if (rawFriendData.terms === undefined) { + return updateFriendData((draft) => { + draft.terms = { [currentTerm]: castDraft(defaultFriendTermData) }; + }); + } + const currentFriendTermData = rawFriendData.terms[currentTerm]; + const correctedFriendTermData = + currentFriendTermData === undefined || + currentFriendTermData.accessibleSchedules === undefined + ? defaultFriendTermData + : currentFriendTermData; + + if (correctedFriendTermData !== currentFriendTermData) { + updateFriendData((draft) => { + draft.terms[currentTerm] = castDraft(correctedFriendTermData); + }); + } + }, [currentTerm, rawFriendData.terms, updateFriendData]); + + // Create a nested update callback for just the friend term data. + const updateFriendTermData = useCallback( + ( + applyDraft: ( + draft: Draft + ) => void | Immutable + ): void => { + updateFriendData((draft) => { + const currentFriendTermDataDraft = draft.terms[currentTerm] ?? null; + if ( + currentFriendTermDataDraft === null || + currentFriendTermDataDraft.accessibleSchedules === undefined + ) { + softError( + new ErrorWithFields({ + message: + 'updateFriendTermData called with invalid current term; ignoring', + fields: { + currentTerm, + currentFriendTermData: null, + }, + }) + ); + return; + } + + draft.terms[currentTerm] = produce( + currentFriendTermDataDraft, + (subDraft) => castDraft(applyDraft(subDraft)) + ); + }); + }, + [updateFriendData, currentTerm] + ); + + const currentFriendTermData = rawFriendData.terms + ? rawFriendData.terms[currentTerm] + : undefined; + + if ( + currentFriendTermData === undefined || + currentFriendTermData.accessibleSchedules === undefined + ) { + return { type: 'loading' }; + } + + return { + type: 'loaded', + result: { + termFriendData: currentFriendTermData.accessibleSchedules, + updateFriendTermData, + }, + }; +} diff --git a/src/data/hooks/useExtractScheduleVersion.ts b/src/data/hooks/useExtractScheduleVersion.ts index 434337c9..e34a497a 100644 --- a/src/data/hooks/useExtractScheduleVersion.ts +++ b/src/data/hooks/useExtractScheduleVersion.ts @@ -62,6 +62,7 @@ export default function useExtractScheduleVersion({ const id = generateScheduleVersionId(); draft.versions[id] = { name: 'Primary', + friends: {}, createdAt: new Date().toISOString(), schedule: castDraft(defaultSchedule), }; diff --git a/src/data/hooks/useExtractTermScheduleData.ts b/src/data/hooks/useExtractTermScheduleData.ts index 13d492db..0864798e 100644 --- a/src/data/hooks/useExtractTermScheduleData.ts +++ b/src/data/hooks/useExtractTermScheduleData.ts @@ -73,7 +73,7 @@ export default function useExtractTermScheduleData({ 'updateTermScheduleData called on term that does not exist', fields: { currentTerm, - currentTermScheduleData, + currentTermScheduleData: currentTermScheduleDataDraft, allTermsInData: Object.keys(draft.terms), }, }) @@ -87,7 +87,7 @@ export default function useExtractTermScheduleData({ ); }); }, - [currentTerm, currentTermScheduleData, updateScheduleData] + [currentTerm, updateScheduleData] ); if (currentTermScheduleData === undefined) { diff --git a/src/data/hooks/useFirebaseAuth.ts b/src/data/hooks/useFirebaseAuth.ts index 054e0022..3053b166 100644 --- a/src/data/hooks/useFirebaseAuth.ts +++ b/src/data/hooks/useFirebaseAuth.ts @@ -32,11 +32,26 @@ export default function useFirebaseAuth(): LoadingState { name: user.displayName, email: user.email, id: user.uid, + getToken: (): Promise => { + const { currentUser } = firebase.auth(); + if (!currentUser) { + return Promise.reject( + new ErrorWithFields({ + message: 'firebase.auth().currentUser is null', + }) + ); + } + return currentUser.getIdToken(); + }, provider, signOut: () => { firebase .auth() .signOut() + .then(() => { + // don't want to share localStorage between accounts + localStorage.clear(); + }) .catch((err) => { softError( new ErrorWithFields({ diff --git a/src/data/hooks/useFriendDataProducer.ts b/src/data/hooks/useFriendDataProducer.ts new file mode 100644 index 00000000..0cf255bf --- /dev/null +++ b/src/data/hooks/useFriendDataProducer.ts @@ -0,0 +1,41 @@ +import produce, { Draft, Immutable } from 'immer'; +import { useCallback } from 'react'; + +import { FriendData } from '../types'; + +type HookResult = { + updateFriendData: ( + applyDraft: (draft: Draft) => void | Immutable + ) => void; +}; + +/** + * Constructs the Immer producer + * from the raw schedule data state setter. + * Returns a referentially stable callback function + * that can be used to update the schedule data using an immer draft: + * https://immerjs.github.io/immer/produce/ + */ +export default function useFriendDataProducer({ + setFriendData, +}: { + setFriendData: ( + next: ((current: FriendData | null) => FriendData | null) | FriendData + ) => void; +}): HookResult { + const updateFriendData = useCallback( + (applyDraft: (draft: Draft) => void): void => + // Here, we use the callback API for the setter function + // returned by `useState` so that we don't have to re-generate + // the callback when the state changes + setFriendData((current: FriendData | null) => { + // Use `produce` from Immer to combine the current state + // & caller-supplied callback that modifies the current state + // to produce the next state + return produce(current, applyDraft); + }), + [setFriendData] + ); + + return { updateFriendData }; +} diff --git a/src/data/hooks/useMigrateScheduleData.test.ts b/src/data/hooks/useMigrateScheduleData.test.ts index 7cb66cf4..8362ede3 100644 --- a/src/data/hooks/useMigrateScheduleData.test.ts +++ b/src/data/hooks/useMigrateScheduleData.test.ts @@ -83,6 +83,7 @@ describe('useMigrateScheduleData', () => { name: 'Primary', // January 1, 1970 at 0 seconds createdAt: '1970-01-01T00:00:00.000Z', + friends: {}, schedule: { desiredCourses: ['CS 1100', 'CS 1331'], pinnedCrns: [ @@ -140,6 +141,7 @@ describe('useMigrateScheduleData', () => { sv_48RC7kqO7YDiBK66qXOd: { name: 'Primary', createdAt: '2021-09-16T00:00:46.191Z', + friends: {}, schedule: { desiredCourses: ['CS 1100', 'CS 1331'], pinnedCrns: [ @@ -186,6 +188,7 @@ describe('useMigrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, diff --git a/src/data/hooks/useRawFriendDataFromFirebase.ts b/src/data/hooks/useRawFriendDataFromFirebase.ts new file mode 100644 index 00000000..1ccf05fe --- /dev/null +++ b/src/data/hooks/useRawFriendDataFromFirebase.ts @@ -0,0 +1,194 @@ +import { Immutable, castImmutable, castDraft } from 'immer'; +import { useCallback, useEffect, useState } from 'react'; + +import { SignedIn } from '../../contexts/account'; +import { ErrorWithFields, softError } from '../../log'; +import { + LoadingState, + LoadingStateCustom, + LoadingStateError, +} from '../../types'; +import { db, isAuthEnabled, friendsCollection } from '../firebase'; +import { FriendData, defaultFriendData } from '../types'; + +type HookResult = { + rawFriendData: Immutable; + setFriendData: ( + next: ((current: FriendData | null) => FriendData | null) | FriendData + ) => void; +}; + +type FriendDataState = Loading | NonExistant | FriendDataExists; + +type Loading = { + type: 'loading'; +}; +type NonExistant = { + type: 'nonExistant'; +}; + +type FriendDataExists = { + type: 'exists'; + data: FriendData; +}; + +/** + * Gets the current schedule data from Firebase. + * Do not call this function in a non-root component; + * it should only be called once in a root component (i.e. ). + */ +export default function useRawFriendDataFromFirebase( + account: SignedIn +): LoadingState { + const [friendData, setFriendData] = useState({ + type: 'loading', + }); + + const [permanentError, setPermanentError] = useState< + LoadingStateError | LoadingStateCustom | null + >(null); + useEffect(() => { + if (!isAuthEnabled) return undefined; + + const removeFriendsSnapshotListener = friendsCollection + .doc(account.id) + .onSnapshot( + { + // Ignore metadata changes + includeMetadataChanges: false, + }, + (doc) => { + const data = doc.data(); + if (data == null) { + setFriendData({ type: 'nonExistant' }); + } else { + setFriendData({ + type: 'exists', + data: doc.data() as FriendData, + }); + } + } + ); + return (): void => { + removeFriendsSnapshotListener(); + }; + }, [account.id]); + + const setFriendDataPersistent = useCallback( + ( + next: ((current: FriendData | null) => FriendData | null) | FriendData + ): void => { + let nextFriendData; + setFriendData((state: FriendDataState) => { + if (typeof next === 'function') { + let currentFriendData; + if (state.type === 'exists') { + currentFriendData = state.data; + } else { + currentFriendData = null; + } + nextFriendData = next(currentFriendData); + } else { + nextFriendData = next; + } + if (nextFriendData === null) return state; + + // Eagerly set the friend data here as well. + // It would be okay to wait until Firebase updates the state for us, + // (which it will do, even before the network calls are made), + // but this allows a window where state can react based on stale state. + return { type: 'exists', data: nextFriendData }; + }); + + if (nextFriendData === undefined || nextFriendData === null) return; + friendsCollection + .doc(account.id) + .set(nextFriendData) + .catch((err) => { + softError( + new ErrorWithFields({ + message: 'error when updating remote document', + source: err, + fields: { + accountId: account.id, + }, + }) + ); + }); + }, + [account.id] + ); + + // Perform a transaction if the type is non-existent, + // trying to pull existing data from local storage + // and storing it in Firebase. + // This serves to provide the initial account data. + useEffect(() => { + if (!isAuthEnabled) return; + + if (friendData.type === 'nonExistant') { + // Imperatively get the latest migrated data + const currentFriendData: Immutable = defaultFriendData; + + // Start the transaction + db.runTransaction(async (transaction) => { + const currentDoc = await transaction.get( + friendsCollection.doc(account.id) + ); + if (currentDoc.exists) return; + transaction.set( + friendsCollection.doc(account.id), + castDraft(currentFriendData) + ); + }).catch((err) => { + // Send the error to Sentry + const error = new ErrorWithFields({ + message: 'an error occurred while initializing account friend data', + source: err, + fields: { + account: account.id, + }, + }); + softError(error); + + // Report the error to the user + setPermanentError({ + type: 'error', + error, + stillLoading: false, + overview: String(err), + }); + }); + } + }, [account.id, friendData.type]); + + // If this hook is running and auth is not enabled, + // then something is wrong with the state. + // Show an error. + if (!isAuthEnabled) { + return { + type: 'error', + error: new ErrorWithFields({ + message: 'cannot obtain data from firebase: authentication is disabled', + }), + stillLoading: false, + overview: 'authentication is not enabled', + }; + } + + if (permanentError !== null) { + return permanentError; + } + + if (friendData.type === 'loading' || friendData.type === 'nonExistant') { + return { type: 'loading' }; + } + + return { + type: 'loaded', + result: { + rawFriendData: castImmutable(friendData.data), + setFriendData: setFriendDataPersistent, + }, + }; +} diff --git a/src/data/hooks/useRawFriendScheduleDataFromFirebaseFunction.ts b/src/data/hooks/useRawFriendScheduleDataFromFirebaseFunction.ts new file mode 100644 index 00000000..820c1697 --- /dev/null +++ b/src/data/hooks/useRawFriendScheduleDataFromFirebaseFunction.ts @@ -0,0 +1,266 @@ +import axios, { AxiosPromise } from 'axios'; +import { useState, useRef } from 'react'; +import { Immutable } from 'immer'; + +import { auth } from '../firebase'; +import useRateLimiter from '../../hooks/useRateLimiter'; +import { ErrorWithFields, softError } from '../../log'; +import { LoadingState } from '../../types'; +import { + exponentialBackoff, + isAxiosNetworkError, + sleep, +} from '../../utils/misc'; +import Cancellable from '../../utils/cancellable'; +import { CLOUD_FUNCTION_BASE_URL } from '../../constants'; +import { FriendIds, RawFriendScheduleData } from '../types'; +import useDeepCompareEffect from '../../hooks/useDeepCompareEffect'; + +interface HookResult { + friendScheduleData: RawFriendScheduleData; + term: string; +} + +const url = `${CLOUD_FUNCTION_BASE_URL}/fetchFriendSchedules`; + +export const RATE_LIMITER_BUCKET_STORAGE_KEY = + process.env.NODE_ENV === 'production' && !process.env['REACT_APP_PREVIEW'] + ? 'rate-limiter-bucket' + : 'rate-limiter-bucket-dev'; +const RATE_LIMITER_CAPACITY = 10; +const RATE_LIMITER_INTERVAL_SEC = 10; + +// Number of minutes between re-fetches of the friend schedules +const REFRESH_INTERVAL_MIN = 5; + +/** + * Fetches the schedules of friends that have been shared with the user + * for the given term. + * Repeatedly attempts to load in the case of errors, + * and cancels any in-flight downloads if the parent context is unmounted + * or the term is changed. + * Once loaded, this also attempts to update the data every 5 minutes + * in case the friends' schedules have been updated. + */ +export default function useRawFriendScheduleDataFromFirebaseFunction({ + currentTerm, + termFriendData, +}: { + currentTerm: string; + termFriendData: Immutable; +}): LoadingState { + const [state, setState] = useState>({ + type: 'loading', + }); + + const { hasReachedLimit, refreshBucket, decrementBucketCount } = + useRateLimiter( + RATE_LIMITER_BUCKET_STORAGE_KEY, + RATE_LIMITER_CAPACITY, + RATE_LIMITER_INTERVAL_SEC + ); + + // Keep a ref of the latest loaded schedules + // to check if it is any newer than the current one. + const loadedFriendScheduleRef = useRef(null); + + // Fetch the current term's friend schedules information + useDeepCompareEffect(() => { + if (Object.keys(termFriendData).length === 0) { + const res = { + friendScheduleData: {}, + term: currentTerm, + }; + loadedFriendScheduleRef.current = res; + return setState({ + type: 'loaded', + result: res, + }); + } + const loadOperation = new Cancellable(); + + async function loadAndRefresh(): Promise { + let isFirst = true; + while (!loadOperation.isCancelled) { + // Load the friend schedules, showing errors only if this is the + // first time it is being loaded (otherwise, just log errors + // but don't disrupt the user). This is to prevent + // a background refresh from showing an error screen + // in the middle of a session. + // `load` will return early if it is cancelled + await load({ initialLoad: isFirst }); + if (loadOperation.isCancelled) return; + + // Sleep for the refresh interval, + // exiting early if cancelled + const promise = sleep({ amount_ms: REFRESH_INTERVAL_MIN * 60 * 1000 }); + const result = await loadOperation.perform(promise); + if (result.cancelled) { + return; + } + + isFirst = false; + } + } + + async function load({ + initialLoad, + }: { + initialLoad: boolean; + }): Promise { + if (initialLoad) { + setState({ + type: 'loading', + }); + } + + let attemptNumber = 1; + while (!loadOperation.isCancelled) { + try { + const requestData = JSON.stringify({ + IDToken: await auth.currentUser?.getIdToken(), + friends: termFriendData, + term: currentTerm, + }); + /* eslint-disable max-len */ + // This request should be made with content type is application/x-www-form-urlencoded. + // This is done to prevent a pre-flight CORS request made to the firebase function. + // Refer: https://github.com/gt-scheduler/website/pull/187#issuecomment-1496439246 + /* eslint-enable max-len */ + const promise = axios({ + method: 'POST', + url, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: `data=${requestData}`, + }) as AxiosPromise; + const result = await loadOperation.perform(promise); + if (result.cancelled) { + return; + } + + const json = result.value.data; + + // If the data is the same as the currently loaded data, + // skip loading it + if ( + loadedFriendScheduleRef.current !== null && + loadedFriendScheduleRef.current.friendScheduleData === json && + loadedFriendScheduleRef.current.term === currentTerm + ) { + // Skip this update + return; + } + + const res = { + friendScheduleData: json, + term: currentTerm, + }; + + setState({ + type: 'loaded', + result: res, + }); + loadedFriendScheduleRef.current = res; + + return; + } catch (err) { + // Report the error to Sentry if not a network error + if (!isAxiosNetworkError(err)) { + softError( + new ErrorWithFields({ + message: 'error fetching friend schedules', + source: err, + fields: { + url, + term: currentTerm, + termFriendData, + }, + }) + ); + } + + if (initialLoad) { + // Flag that an error has occurred + setState({ + type: 'error', + error: + err instanceof Error + ? err + : new ErrorWithFields({ + message: + 'an error occurred while fetching friend schedules', + source: err, + }), + stillLoading: true, + overview: String(err), + }); + } + } + + // Sleep for an exponential backoff between each retry + await exponentialBackoff(attemptNumber); + attemptNumber += 1; + } + } + + refreshBucket(); + if (hasReachedLimit) { + const err = new ErrorWithFields({ + message: 'error loading and refreshing friend schedules', + source: new Error('Exceeded rate limit'), + fields: { + url, + term: currentTerm, + termFriendData, + hasReachedLimit, + }, + }); + softError(err); + setState({ + type: 'error', + error: err, + stillLoading: false, + overview: String(err), + }); + } else { + decrementBucketCount(); + loadAndRefresh().catch((err) => { + softError( + new ErrorWithFields({ + message: 'error loading and refreshing friend schedules', + source: err, + fields: { + url, + term: currentTerm, + termFriendData, + hasReachedLimit, + }, + }) + ); + }); + } + + // Cancel the background load when this cleans up + return (): void => { + loadOperation.cancel(); + }; + }, [ + currentTerm, + termFriendData, + setState, + hasReachedLimit, + refreshBucket, + decrementBucketCount, + ]); + + // If we are about to start a new background load + // after the term changed, then don't return the already fetched + // friend schedules + if (state.type === 'loaded' && state.result.term !== currentTerm) { + return { type: 'loading' }; + } + + return state; +} diff --git a/src/data/hooks/useRawScheduleDataFromFirebase.ts b/src/data/hooks/useRawScheduleDataFromFirebase.ts index 21838796..49c3620e 100644 --- a/src/data/hooks/useRawScheduleDataFromFirebase.ts +++ b/src/data/hooks/useRawScheduleDataFromFirebase.ts @@ -80,24 +80,28 @@ export default function useRawScheduleDataFromFirebase( | AnyScheduleData ): void => { let nextScheduleData; - if (typeof next === 'function') { - let currentScheduleData; - if (scheduleData.type === 'exists') { - currentScheduleData = scheduleData.data; + setScheduleData((state: ScheduleDataState) => { + if (typeof next === 'function') { + let currentScheduleData; + if (state.type === 'exists') { + currentScheduleData = state.data; + } else { + currentScheduleData = null; + } + nextScheduleData = next(currentScheduleData); } else { - currentScheduleData = null; + nextScheduleData = next; } - nextScheduleData = next(currentScheduleData); - } else { - nextScheduleData = next; - } - if (nextScheduleData === null) return; + if (nextScheduleData === null) return state; + + // Eagerly set the schedule data here as well. + // It would be okay to wait until Firebase updates the state for us, + // (which it will do, even before the network calls are made), + // but this allows a window where state can react based on stale state. + return { type: 'exists', data: nextScheduleData }; + }); - // Eagerly set the schedule data here as well. - // It would be okay to wait until Firebase updates the state for us, - // (which it will do, even before the network calls are made), - // but this allows a window where state can react based on stale state. - setScheduleData({ type: 'exists', data: nextScheduleData }); + if (nextScheduleData === undefined || nextScheduleData === null) return; schedulesCollection .doc(account.id) @@ -114,7 +118,7 @@ export default function useRawScheduleDataFromFirebase( ); }); }, - [account.id, scheduleData] + [account.id] ); // Perform a transaction if the type is non-existent, diff --git a/src/data/hooks/useVersionActions.ts b/src/data/hooks/useVersionActions.ts index 9e5ab32f..81a394b4 100644 --- a/src/data/hooks/useVersionActions.ts +++ b/src/data/hooks/useVersionActions.ts @@ -13,6 +13,7 @@ export type HookResult = { deleteVersion: (id: string) => void; renameVersion: (id: string, newName: string) => void; cloneVersion: (id: string, newName: string) => void; + deleteFriendRecord: (versionId: string, friendId: string) => void; }; /** @@ -40,6 +41,7 @@ export default function useVersionActions({ updateTermScheduleData((draft) => { draft.versions[id] = { name, + friends: {}, schedule: castDraft(defaultSchedule), createdAt: new Date().toISOString(), }; @@ -90,6 +92,7 @@ export default function useVersionActions({ const newId = generateScheduleVersionId(); draft.versions[newId] = { name: 'Primary', + friends: {}, createdAt: new Date().toISOString(), schedule: castDraft(defaultSchedule), }; @@ -174,6 +177,7 @@ export default function useVersionActions({ } draft.versions[newId] = { name: newName, + friends: {}, schedule: castDraft(existingDraft.schedule), createdAt: new Date().toISOString(), }; @@ -183,5 +187,50 @@ export default function useVersionActions({ [updateTermScheduleData, setVersion] ); - return { addNewVersion, deleteVersion, renameVersion, cloneVersion }; + const deleteFriendRecord = useCallback( + (versionId: string, friendId: string): void => { + updateTermScheduleData((draft) => { + const existingDraft = draft.versions[versionId]; + if (existingDraft === undefined) { + softError( + new ErrorWithFields({ + message: + "deleteFriendRecord called with version name that doesn't exist; ignoring", + fields: { + allVersionNames: Object.entries(draft.versions).map( + ([versionId_, { name }]) => ({ id: versionId_, name }) + ), + versionId, + versionCount: Object.keys(draft.versions).length, + }, + }) + ); + return; + } + if (friendId in existingDraft.friends) { + delete existingDraft.friends[friendId]; + } else { + softError( + new ErrorWithFields({ + message: + "deleteFriendRecord called with friend ID that doesn't exist; ignoring", + fields: { + allFriendIds: Object.keys(existingDraft.friends), + friendId, + }, + }) + ); + } + }); + }, + [updateTermScheduleData] + ); + + return { + addNewVersion, + deleteVersion, + renameVersion, + cloneVersion, + deleteFriendRecord, + }; } diff --git a/src/data/migrations/2to3.test.ts b/src/data/migrations/2to3.test.ts index b30fc690..992504c6 100644 --- a/src/data/migrations/2to3.test.ts +++ b/src/data/migrations/2to3.test.ts @@ -127,6 +127,7 @@ describe('migrate2to3', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, sv_00000000000000000001: { name: 'Secondary', @@ -140,6 +141,7 @@ describe('migrate2to3', () => { colorMap: {}, sortingOptionIndex: 0, }, + friends: {}, }, sv_00000000000000000002: { name: 'Tertiary', @@ -153,6 +155,7 @@ describe('migrate2to3', () => { colorMap: {}, sortingOptionIndex: 0, }, + friends: {}, }, }, }, diff --git a/src/data/migrations/2to3.ts b/src/data/migrations/2to3.ts index 1e5f5e30..291a8048 100644 --- a/src/data/migrations/2to3.ts +++ b/src/data/migrations/2to3.ts @@ -37,6 +37,7 @@ export default function migrate2To3( const version3ScheduleVersion: Version3ScheduleVersion = { name: version2ScheduleVersion.name, createdAt: version2ScheduleVersion.createdAt, + friends: {}, schedule: { ...version2ScheduleVersion.schedule, ...newFields, diff --git a/src/data/migrations/index.test.ts b/src/data/migrations/index.test.ts index 48c0684d..6336bcde 100644 --- a/src/data/migrations/index.test.ts +++ b/src/data/migrations/index.test.ts @@ -62,6 +62,7 @@ describe('migrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, @@ -131,6 +132,7 @@ describe('migrateScheduleData', () => { colorMap: {}, sortingOptionIndex: 0, }, + friends: {}, }, }, }, @@ -155,6 +157,7 @@ describe('migrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, @@ -222,6 +225,7 @@ describe('migrateScheduleData', () => { colorMap: {}, sortingOptionIndex: 0, }, + friends: {}, }, }, }, @@ -245,6 +249,7 @@ describe('migrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, diff --git a/src/data/types.ts b/src/data/types.ts index 6f955933..d3ab1272 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -1,4 +1,4 @@ -import { Immutable } from 'immer'; +import { castImmutable, Immutable } from 'immer'; import { Event } from '../types'; import { generateRandomId } from '../utils/misc'; @@ -39,6 +39,27 @@ export const defaultScheduleData: Immutable = { version: 3, }; +export const defaultFriendData: Immutable = { + terms: {}, + info: {}, +}; + +export const defaultFriendInfo: Immutable<{ + name: string; + email: string; +}> = castImmutable({ + name: '', + email: '', +}); + +export const defaultFriendTermData: Immutable = { + accessibleSchedules: {}, +}; + +export const defaultFriendScheduleData: Immutable = { + // +}; + export const defaultTermScheduleData: Immutable = { versions: {}, }; @@ -130,10 +151,16 @@ export interface Version3TermScheduleData { export interface Version3ScheduleVersion { name: string; + friends: Record; createdAt: string; schedule: Version3Schedule; } +export interface FriendShareData { + status: 'Pending' | 'Accepted'; + email: string; +} + export interface Version3Schedule { desiredCourses: string[]; pinnedCrns: string[]; @@ -142,3 +169,54 @@ export interface Version3Schedule { colorMap: Record; sortingOptionIndex: number; } + +export type FriendIds = Record; + +export interface FriendTermData { + accessibleSchedules: FriendIds; +} + +export type ApiErrorResponse = { + message: string; +}; + +export type FriendInfo = Record< + string, + { + name: string; + email: string; + } +>; + +export interface FriendData { + terms: Record; + info: FriendInfo; +} + +export type RawFriendScheduleData = Record< + string, + { + versions: Record< + string, + { + name: string; + schedule: Schedule; + } + >; + } +>; + +export type FriendScheduleData = Record< + string, + { + name: string; + email: string; + versions: Record< + string, + { + name: string; + schedule: Schedule; + } + >; + } +>; diff --git a/src/hooks/useDeepCompareEffect.ts b/src/hooks/useDeepCompareEffect.ts new file mode 100644 index 00000000..f1730456 --- /dev/null +++ b/src/hooks/useDeepCompareEffect.ts @@ -0,0 +1,31 @@ +import { useEffect, EffectCallback, DependencyList, useRef } from 'react'; +import lodash from 'lodash'; + +/** + * Inspired by https://github.com/kentcdodds/use-deep-compare-effect + * React useEffect performs a reference equality check for its non-primitive + * dependencies. + * + * If obj is an object that is a state, and useEffect has a dependency + * on obj.field, where obj.field is non-primitive, issues can arise. + * The Effect will re-run on every state change to obj even if the value of + * obj.field remains the same, since the state change changed the reference + * of obj. + * + * This hook keeps track of the previous dependency list and performs a deep + * comparison with the new dependency list. The Effect is re-run only if the + * deep comparison is false. + */ +export default function useDeepCompareEffect( + callback: EffectCallback, + dependencies: DependencyList +): void { + const ref = useRef([]); + + if (!lodash.isEqual(dependencies, ref.current)) { + ref.current = lodash.cloneDeep(dependencies); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useEffect(callback, ref.current); +} diff --git a/src/hooks/useRateLimiter.ts b/src/hooks/useRateLimiter.ts new file mode 100644 index 00000000..a7ca76af --- /dev/null +++ b/src/hooks/useRateLimiter.ts @@ -0,0 +1,80 @@ +import { useMemo, useCallback } from 'react'; +import useLocalStorageState from 'use-local-storage-state'; + +interface RateLimiterBucket { + remainingCount: number; + lastRefreshTime: string | Date; +} + +export default function useRateLimiter( + bucketName: string, + capacity: number, + interval: number +): { + hasReachedLimit: boolean; + refreshBucket: () => void; + decrementBucketCount: () => void; +} { + const [bucket, setBucket] = useLocalStorageState( + bucketName, + { + defaultValue: { + remainingCount: capacity, + lastRefreshTime: new Date(), + }, + storageSync: true, + } + ); + + const intervalMs = useMemo(() => interval * 1000, [interval]); + + const hasReachedLimit = useMemo(() => { + return bucket.remainingCount < 0; + }, [bucket.remainingCount]); + + const refreshBucket = useCallback(() => { + setBucket((currBucket) => { + const oldDate = new Date(currBucket.lastRefreshTime); + const newDate = new Date(); + const isOldDateInvalid = Number.isNaN(oldDate.valueOf()); + if (!isOldDateInvalid) { + const bucketCountAdded = Math.floor( + ((newDate.valueOf() - oldDate.valueOf()) / intervalMs) * capacity + ); + if (bucketCountAdded > 0) { + return { + remainingCount: Math.min( + capacity, + currBucket.remainingCount + + bucketCountAdded + + (currBucket.remainingCount < 0 ? 1 : 0) + ), + lastRefreshTime: newDate, + }; + } + } + + return { + remainingCount: Math.min(capacity, currBucket.remainingCount), + lastRefreshTime: isOldDateInvalid + ? new Date() + : currBucket.lastRefreshTime, + }; + }); + }, [capacity, intervalMs, setBucket]); + + const decrementBucketCount = useCallback(() => { + setBucket((currBucket) => { + return { + ...currBucket, + remainingCount: currBucket.remainingCount - 1, + }; + }); + }, [setBucket]); + + return { + hasReachedLimit, + refreshBucket, + decrementBucketCount, + }; +} diff --git a/src/index.tsx b/src/index.tsx index eee92b7f..003c1795 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client'; import * as Sentry from '@sentry/react'; import { Integrations } from '@sentry/tracing'; -import App from './components/App'; +import RouterComponent from './components/RouterComponent'; import { ErrorWithFields } from './log'; import 'normalize.css'; @@ -31,4 +31,4 @@ if (container === null) { }); } const root = createRoot(container); -root.render(); +root.render(); diff --git a/src/types.ts b/src/types.ts index 2302a8bb..f496416c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -354,3 +354,27 @@ export interface CrawlerTermData { */ version: number; } + +export type ScheduleDeletionRequest = { + /** + * token of account that requested the schedule deletion + */ + IDToken: string | void; + /** + * ID of the INVITEE if the deletion requester is the INVITER + * ID of the INVITER if the deletion requester is the INVITEE + */ + peerUserId: string; + /** + * term that schedule version(s) belong to + */ + term: string; + /** + * shared schedule version(s) for deletion + */ + versions: string[] | string; + /** + * whether the schedule version belongs to the requester + */ + owner: boolean; +}; diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index 52b921f3..ef684930 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -279,6 +279,9 @@ const getDateRange = (term: string): DateRange => { return { from, to }; }; +// Difference between UTC and EST timezones in minutes +export const EST_TIMEZONE_OFFSET = 240; + /** * Exports the current schedule to a `.ics` file, * which allows for importing into a third-party calendar application. @@ -302,6 +305,8 @@ export function exportCoursesToCalendar( return; } + const timezoneDiff = EST_TIMEZONE_OFFSET - new Date().getTimezoneOffset(); + const addEventsToCalendar = ( period: Period, days: string[], @@ -317,9 +322,11 @@ export function exportCoursesToCalendar( ) { begin.setDate(begin.getDate() + 1); } - begin.setHours(period.start / 60, period.start % 60); + const startWithOffset = period.start + timezoneDiff; + const endWithOffset = period.end + timezoneDiff; + begin.setHours(startWithOffset / 60, startWithOffset % 60); const end = new Date(begin.getTime()); - end.setHours(period.end / 60, period.end % 60); + end.setHours(endWithOffset / 60, endWithOffset % 60); const rrule = { freq: 'WEEKLY', until: to, diff --git a/yarn.lock b/yarn.lock index 1ba506cf..e6df0392 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2071,6 +2071,11 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@remix-run/router@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.10.0.tgz#e2170dc2049b06e65bbe883adad0e8ddf8291278" + integrity sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw== + "@restart/hooks@^0.4.7": version "0.4.7" resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39" @@ -2566,6 +2571,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/lodash@^4.14.192": + version "4.14.192" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285" + integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A== + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -9744,6 +9754,21 @@ react-resize-panel@^0.3.5: lodash.debounce "^4.0.8" react-draggable "^4.0.3" +react-router-dom@^6.17.0: + version "6.17.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.17.0.tgz#ea73f89186546c1cf72b10fcb7356d874321b2ad" + integrity sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ== + dependencies: + "@remix-run/router" "1.10.0" + react-router "6.17.0" + +react-router@6.17.0: + version "6.17.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.17.0.tgz#7b680c4cefbc425b57537eb9c73bedecbdc67c1e" + integrity sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA== + dependencies: + "@remix-run/router" "1.10.0" + react-scripts@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
    {popoverInfo.name}