From d19cd13a1ef041f48da07598dba3e0f700d5dc7b Mon Sep 17 00:00:00 2001 From: Yatharth Bhargava Date: Thu, 2 Mar 2023 03:23:01 -0500 Subject: [PATCH 1/7] lint --- src/components/AppDataLoader/index.tsx | 21 ++-- src/components/AppDataLoader/stages.tsx | 1 - src/components/HeaderActionBar/index.tsx | 16 +-- src/components/InvitationModal/index.tsx | 101 +++++++++++++----- src/components/InviteBackLink/index.tsx | 20 ++-- src/contexts/account.ts | 1 + src/contexts/schedule.ts | 20 +++- src/data/hooks/useExtractScheduleVersion.ts | 1 + src/data/hooks/useFirebaseAuth.ts | 20 ++++ src/data/hooks/useMigrateScheduleData.test.ts | 2 + src/data/hooks/useVersionActions.ts | 41 ++++++- src/data/migrations/2to3.ts | 1 + src/data/types.ts | 6 ++ 13 files changed, 199 insertions(+), 52 deletions(-) diff --git a/src/components/AppDataLoader/index.tsx b/src/components/AppDataLoader/index.tsx index 302d9ec1..96e362fe 100644 --- a/src/components/AppDataLoader/index.tsx +++ b/src/components/AppDataLoader/index.tsx @@ -286,12 +286,17 @@ function ContextProvider({ }, [termScheduleData.versions]); // Get all version-related actions - const { addNewVersion, deleteVersion, renameVersion, cloneVersion } = - useVersionActions({ - updateTermScheduleData, - setVersion, - currentVersion, - }); + const { + addNewVersion, + deleteVersion, + renameVersion, + cloneVersion, + updateFriends, + } = useVersionActions({ + updateTermScheduleData, + setVersion, + currentVersion, + }); // Memoize the context values so that they are stable const scheduleContextValue = useMemo( @@ -301,6 +306,7 @@ function ContextProvider({ oscar, currentVersion, allVersionNames, + currentFriends: scheduleVersion.friends, ...castDraft(scheduleVersion.schedule), }, { @@ -310,6 +316,7 @@ function ContextProvider({ setCurrentVersion: setVersion, addNewVersion, deleteVersion, + updateFriends, renameVersion, cloneVersion, }, @@ -319,12 +326,14 @@ function ContextProvider({ oscar, currentVersion, allVersionNames, + scheduleVersion.friends, scheduleVersion.schedule, setTerm, patchSchedule, updateSchedule, setVersion, addNewVersion, + updateFriends, deleteVersion, renameVersion, cloneVersion, diff --git a/src/components/AppDataLoader/stages.tsx b/src/components/AppDataLoader/stages.tsx index 9fdd11c9..213e0b7f 100644 --- a/src/components/AppDataLoader/stages.tsx +++ b/src/components/AppDataLoader/stages.tsx @@ -256,7 +256,6 @@ export function StageLoadRawScheduleDataFromFirebase({ children, }: StageLoadRawScheduleDataFromFirebaseProps): React.ReactElement { const loadingState = useRawScheduleDataFromFirebase(accountState); - if (loadingState.type !== 'loaded') { return ( diff --git a/src/components/HeaderActionBar/index.tsx b/src/components/HeaderActionBar/index.tsx index b27fadfc..606e798d 100644 --- a/src/components/HeaderActionBar/index.tsx +++ b/src/components/HeaderActionBar/index.tsx @@ -88,13 +88,15 @@ export default function HeaderActionBar({ onClick: onCopyCrns, }); } - exportActions.push({ - label: 'Share Schedule', - icon: faShare, - onClick: (): void => { - setInvitationOpen(true); - }, - }); + if (accountState.type === 'signedIn') { + exportActions.push({ + label: 'Share Schedule', + icon: faShare, + onClick: (): void => { + setInvitationOpen(true); + }, + }); + } // On small mobile screens and on large desktop, // left-anchor the "Export" dropdown. diff --git a/src/components/InvitationModal/index.tsx b/src/components/InvitationModal/index.tsx index 3d2df628..458d1736 100644 --- a/src/components/InvitationModal/index.tsx +++ b/src/components/InvitationModal/index.tsx @@ -3,15 +3,20 @@ import React, { KeyboardEvent, useCallback, useMemo, + useContext, useState, } from 'react'; import { Tooltip as ReactTooltip } from 'react-tooltip'; import { faCircle, faClose } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { castDraft } from 'immer'; +import axios, { AxiosError } from 'axios'; +import { ScheduleContext } from '../../contexts'; import { classes } from '../../utils/misc'; import Modal from '../Modal'; import Button from '../Button'; +import { AccountContext, SignedIn } from '../../contexts/account'; import './stylesheet.scss'; @@ -19,26 +24,18 @@ import './stylesheet.scss'; * Inner content of the invitation modal. */ export function InvitationModalContent(): React.ReactElement { - // Array for testing style of shared emails - // eslint-disable-next-line - const [emails, setEmails] = useState([ - ['user1@example.com', 'Pending'], - ['user2@example.com', 'Accepted'], - ['ReallyLongNameThatWillNotFitInRowAbove@example.com', 'Accepted'], - ['goodEmail@gmail.com', 'Accepted'], - ['user12@example.com', 'Pending'], - ['user22@example.com', 'Accepted'], - ['2ReallyLongNameThatWillNotFitInRowAbove@example.com', 'Accepted'], - ['2goodEmail@gmail.com', 'Accepted'], - ]); + const [{ currentFriends, currentVersion, term }, { updateFriends }] = + useContext(ScheduleContext); + const accountContext = useContext(AccountContext); + + const emails = Object.keys(currentFriends ?? {}).map((friend: string) => { + return [ + currentFriends[friend]!.email, + currentFriends[friend]!.status, + friend, + ]; + }); - // Array to test invalid email - const validUsers = [ - 'user1@example.com', - 'user2@example.com', - 'ReallyLongNameThatWillNotFitInRowAbove@example.com', - 'goodEmail@gmail.com', - ]; const [input, setInput] = useState(''); const [validMessage, setValidMessage] = useState(''); const [validClassName, setValidClassName] = useState(''); @@ -122,16 +119,61 @@ export function InvitationModalContent(): React.ReactElement { [searchResults] ); - function verifyUser(): void { - if (validUsers.includes(input)) { - setValidMessage('Successfully sent!'); - setValidClassName('valid-email'); - setInput(''); + const sendInvitation = async (): Promise => { + const IdToken = await (accountContext as SignedIn).getToken(); + axios + .post( + 'http://127.0.0.1:5001/gt-scheduler-web-dev/us-central1/createFriendInvitation', + { + term, + friendEmail: input, + IDToken: IdToken, + version: currentVersion, + } + ) + .then((res) => { + setValidMessage('Successfully sent!'); + setValidClassName('valid-email'); + }) + .catch((err: AxiosError) => { + setValidClassName('invalid-email'); + // if (err.response && err.response.data.message) { + // setValidMessage(err.response.data.message); + // return; + // } + + setValidMessage('Error sending invitation. Please try again later.'); + }); + }; + + // verify email with a regex and send invitation if valid + const verifyEmail = (): void => { + if ( + // eslint-disable-next-line + /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + input + ) + ) { + sendInvitation() + .then(() => { + setInput(''); + }) + .catch((err) => { + setValidMessage('Error sending invitation. Please try again later.'); + setValidClassName('invalid-email'); + }); } else { setValidMessage('Invalid Email'); setValidClassName('invalid-email'); } - } + }; + + // delete friend from record of friends + const handleDelete = (friendId: string): void => { + const newFriends = castDraft(currentFriends); + delete newFriends[friendId]; + updateFriends(currentVersion, newFriends); + }; return (
@@ -174,7 +216,7 @@ export function InvitationModalContent(): React.ReactElement { )} {validMessage}
- @@ -189,7 +231,12 @@ export function InvitationModalContent(): React.ReactElement {
{element[0]} - diff --git a/src/components/InviteBackLink/index.tsx b/src/components/InviteBackLink/index.tsx index c14a4984..58fe15e7 100644 --- a/src/components/InviteBackLink/index.tsx +++ b/src/components/InviteBackLink/index.tsx @@ -7,15 +7,17 @@ import { CLOUD_FUNCTION_BASE_URL } from '../../constants'; const handleInvite = async (inviteId: string | undefined): Promise => // The link should be changed to prod link, or we can choose the link based // on environment - axios.post( - `${CLOUD_FUNCTION_BASE_URL}/handleFriendInvitation`, - { inviteId }, - { - headers: { - 'Content-Type': 'application/json', - }, - } - ); + axios + .post( + `http://127.0.0.1:5001/gt-scheduler-web-dev/us-central1/handleFriendInvitation`, + { inviteId } + ) + .then((res) => { + console.log(res); + }) + .catch((err) => { + console.error(err); + }); export default function InviteBackLink(): React.ReactElement { const navigate = useNavigate(); 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/schedule.ts b/src/contexts/schedule.ts index 12b0a8d5..d0d0e5e8 100644 --- a/src/contexts/schedule.ts +++ b/src/contexts/schedule.ts @@ -3,12 +3,13 @@ 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 } from '../data/types'; import { ErrorWithFields } from '../log'; type ExtraData = { term: string; currentVersion: string; + currentFriends: Record; allVersionNames: { id: string; name: string }[]; // `oscar` is included below as a separate type }; @@ -28,6 +29,10 @@ export type ScheduleContextSetters = { deleteVersion: (id: string) => void; renameVersion: (id: string, newName: string) => void; cloneVersion: (id: string, newName: string) => void; + updateFriends: ( + versionId: string, + newFriends: Record + ) => void; }; export type ScheduleContextValue = [ ScheduleContextData, @@ -37,6 +42,7 @@ export const ScheduleContext = React.createContext([ { term: '', currentVersion: '', + currentFriends: {}, allVersionNames: [], oscar: EMPTY_OSCAR, ...defaultSchedule, @@ -71,6 +77,18 @@ export const ScheduleContext = React.createContext([ }, }); }, + updateFriends: ( + versionId: string, + newFriends: Record + ): void => { + throw new ErrorWithFields({ + message: 'empty ScheduleContext.deleteFriendRecord value being used', + fields: { + versionId, + newFriends, + }, + }); + }, addNewVersion: (name: string, select?: boolean): string => { throw new ErrorWithFields({ message: 'empty ScheduleContext.addNewVersion value being used', 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/useFirebaseAuth.ts b/src/data/hooks/useFirebaseAuth.ts index 054e0022..6e66893c 100644 --- a/src/data/hooks/useFirebaseAuth.ts +++ b/src/data/hooks/useFirebaseAuth.ts @@ -32,6 +32,26 @@ export default function useFirebaseAuth(): LoadingState { name: user.displayName, email: user.email, id: user.uid, + getToken: (): Promise => { + if (firebase.auth().currentUser === null) { + return Promise.reject( + new ErrorWithFields({ + message: 'firebase.auth().currentUser is null', + }) + ); + } + return firebase + .auth() + .currentUser!.getIdToken() + .catch((err) => { + softError( + new ErrorWithFields({ + message: 'call to firebase.auth().getIdToken() failed', + source: err, + }) + ); + }); + }, provider, signOut: () => { firebase diff --git a/src/data/hooks/useMigrateScheduleData.test.ts b/src/data/hooks/useMigrateScheduleData.test.ts index 7cb66cf4..1568cb3e 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: [ diff --git a/src/data/hooks/useVersionActions.ts b/src/data/hooks/useVersionActions.ts index 19a1ea44..c3a771d4 100644 --- a/src/data/hooks/useVersionActions.ts +++ b/src/data/hooks/useVersionActions.ts @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { softError, ErrorWithFields } from '../../log'; import { defaultSchedule, + FriendShareData, generateScheduleVersionId, TermScheduleData, } from '../types'; @@ -13,6 +14,10 @@ export type HookResult = { deleteVersion: (id: string) => void; renameVersion: (id: string, newName: string) => void; cloneVersion: (id: string, newName: string) => void; + updateFriends: ( + versionId: string, + newFriends: Record + ) => void; }; /** @@ -40,6 +45,7 @@ export default function useVersionActions({ updateTermScheduleData((draft) => { draft.versions[id] = { name, + friends: {}, schedule: castDraft(defaultSchedule), createdAt: new Date().toISOString(), }; @@ -90,6 +96,7 @@ export default function useVersionActions({ const newId = generateScheduleVersionId(); draft.versions[newId] = { name: 'Primary', + friends: {}, createdAt: new Date().toISOString(), schedule: castDraft(defaultSchedule), }; @@ -181,5 +188,37 @@ export default function useVersionActions({ [updateTermScheduleData, addNewVersion] ); - return { addNewVersion, deleteVersion, renameVersion, cloneVersion }; + const updateFriends = useCallback( + (versionId: string, newFriends: Record): 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; + } + existingDraft.friends = castDraft(newFriends); + }); + }, + [updateTermScheduleData] + ); + + return { + addNewVersion, + deleteVersion, + renameVersion, + cloneVersion, + updateFriends, + }; } 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/types.ts b/src/data/types.ts index d1af52b1..055ed213 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -134,10 +134,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[]; From 490718bed3da28443b39f6ba0e802806b28d9954 Mon Sep 17 00:00:00 2001 From: Yatharth Bhargava Date: Thu, 2 Mar 2023 15:59:00 -0500 Subject: [PATCH 2/7] lint --- src/components/InvitationModal/index.tsx | 46 +++++++++++------------- src/components/InviteBackLink/index.tsx | 23 ++++++------ src/data/hooks/useFirebaseAuth.ts | 22 ++++++------ src/data/types.ts | 4 +++ 4 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/components/InvitationModal/index.tsx b/src/components/InvitationModal/index.tsx index 458d1736..3eeb2d7e 100644 --- a/src/components/InvitationModal/index.tsx +++ b/src/components/InvitationModal/index.tsx @@ -12,6 +12,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { castDraft } from 'immer'; import axios, { AxiosError } from 'axios'; +import { ApiErrorResponse } from '../../data/types'; import { ScheduleContext } from '../../contexts'; import { classes } from '../../utils/misc'; import Modal from '../Modal'; @@ -28,14 +29,6 @@ export function InvitationModalContent(): React.ReactElement { useContext(ScheduleContext); const accountContext = useContext(AccountContext); - const emails = Object.keys(currentFriends ?? {}).map((friend: string) => { - return [ - currentFriends[friend]!.email, - currentFriends[friend]!.status, - friend, - ]; - }); - const [input, setInput] = useState(''); const [validMessage, setValidMessage] = useState(''); const [validClassName, setValidClassName] = useState(''); @@ -131,16 +124,18 @@ export function InvitationModalContent(): React.ReactElement { version: currentVersion, } ) - .then((res) => { + .then(() => { setValidMessage('Successfully sent!'); setValidClassName('valid-email'); }) - .catch((err: AxiosError) => { + .catch((err) => { setValidClassName('invalid-email'); - // if (err.response && err.response.data.message) { - // setValidMessage(err.response.data.message); - // return; - // } + 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.'); }); @@ -150,15 +145,13 @@ export function InvitationModalContent(): React.ReactElement { const verifyEmail = (): void => { if ( // eslint-disable-next-line - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( - input - ) + /^\S+@\S+\.\S+$/.test(input) ) { sendInvitation() .then(() => { setInput(''); }) - .catch((err) => { + .catch(() => { setValidMessage('Error sending invitation. Please try again later.'); setValidClassName('invalid-email'); }); @@ -227,27 +220,30 @@ export function InvitationModalContent(): React.ReactElement { Users Invited to View Primary

- {emails.map((element) => ( -
-
- {element[0]} + {Object.keys(currentFriends).map((friend) => ( +
+
+ {currentFriends[friend]?.email} - Status: {element[1]} + Status: {currentFriends[friend]?.status}
diff --git a/src/components/InviteBackLink/index.tsx b/src/components/InviteBackLink/index.tsx index 58fe15e7..e05d8a02 100644 --- a/src/components/InviteBackLink/index.tsx +++ b/src/components/InviteBackLink/index.tsx @@ -2,22 +2,21 @@ import React, { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import axios from 'axios'; -import { CLOUD_FUNCTION_BASE_URL } from '../../constants'; +// import { CLOUD_FUNCTION_BASE_URL } from '../../constants'; const handleInvite = async (inviteId: string | undefined): Promise => // The link should be changed to prod link, or we can choose the link based // on environment - axios - .post( - `http://127.0.0.1:5001/gt-scheduler-web-dev/us-central1/handleFriendInvitation`, - { inviteId } - ) - .then((res) => { - console.log(res); - }) - .catch((err) => { - console.error(err); - }); + axios.post( + `http://127.0.0.1:5001/gt-scheduler-web-dev/us-central1/handleFriendInvitation`, + { inviteId } + ); +// .then((res) => { +// console.log(res); +// }) +// .catch((err) => { +// console.error(err); +// }); export default function InviteBackLink(): React.ReactElement { const navigate = useNavigate(); diff --git a/src/data/hooks/useFirebaseAuth.ts b/src/data/hooks/useFirebaseAuth.ts index 6e66893c..ede940c7 100644 --- a/src/data/hooks/useFirebaseAuth.ts +++ b/src/data/hooks/useFirebaseAuth.ts @@ -33,24 +33,22 @@ export default function useFirebaseAuth(): LoadingState { email: user.email, id: user.uid, getToken: (): Promise => { - if (firebase.auth().currentUser === null) { + const { currentUser } = firebase.auth(); + if (!currentUser) { return Promise.reject( new ErrorWithFields({ message: 'firebase.auth().currentUser is null', }) ); } - return firebase - .auth() - .currentUser!.getIdToken() - .catch((err) => { - softError( - new ErrorWithFields({ - message: 'call to firebase.auth().getIdToken() failed', - source: err, - }) - ); - }); + return currentUser.getIdToken().catch((err) => { + softError( + new ErrorWithFields({ + message: 'call to firebase.auth().getIdToken() failed', + source: err, + }) + ); + }); }, provider, signOut: () => { diff --git a/src/data/types.ts b/src/data/types.ts index 055ed213..9766eaa6 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -160,3 +160,7 @@ export interface FriendData { export interface FriendTermData { accessibleSchedules: Record; } + +export type ApiErrorResponse = { + message: string; +}; From 5aa39ca2e7c54c257ff14ac6a1e4c3eb0cb7c6a8 Mon Sep 17 00:00:00 2001 From: Yatharth Bhargava Date: Mon, 27 Mar 2023 11:46:11 -0400 Subject: [PATCH 3/7] fix tests --- src/data/hooks/useMigrateScheduleData.test.ts | 1 + src/data/migrations/2to3.test.ts | 3 +++ src/data/migrations/index.test.ts | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/src/data/hooks/useMigrateScheduleData.test.ts b/src/data/hooks/useMigrateScheduleData.test.ts index 1568cb3e..8362ede3 100644 --- a/src/data/hooks/useMigrateScheduleData.test.ts +++ b/src/data/hooks/useMigrateScheduleData.test.ts @@ -188,6 +188,7 @@ describe('useMigrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, 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/index.test.ts b/src/data/migrations/index.test.ts index 48c0684d..9c157b74 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: {}, }, }, }, @@ -155,6 +156,7 @@ describe('migrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, @@ -222,6 +224,7 @@ describe('migrateScheduleData', () => { colorMap: {}, sortingOptionIndex: 0, }, + friends: {}, }, }, }, @@ -245,6 +248,7 @@ describe('migrateScheduleData', () => { colorMap: { 'CS 1100': '#0062B1', 'CS 1331': '#194D33' }, sortingOptionIndex: 0, }, + friends: {}, }, }, }, From 7ed7ad893121077c27d9d2b796b15d06929f9c96 Mon Sep 17 00:00:00 2001 From: Yatharth Bhargava Date: Mon, 27 Mar 2023 11:56:12 -0400 Subject: [PATCH 4/7] fix all tests --- src/components/AppDataLoader/index.tsx | 2 +- src/data/migrations/index.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AppDataLoader/index.tsx b/src/components/AppDataLoader/index.tsx index 96e362fe..b42bcfca 100644 --- a/src/components/AppDataLoader/index.tsx +++ b/src/components/AppDataLoader/index.tsx @@ -306,7 +306,7 @@ function ContextProvider({ oscar, currentVersion, allVersionNames, - currentFriends: scheduleVersion.friends, + currentFriends: scheduleVersion.friends ?? {}, ...castDraft(scheduleVersion.schedule), }, { diff --git a/src/data/migrations/index.test.ts b/src/data/migrations/index.test.ts index 9c157b74..6336bcde 100644 --- a/src/data/migrations/index.test.ts +++ b/src/data/migrations/index.test.ts @@ -132,6 +132,7 @@ describe('migrateScheduleData', () => { colorMap: {}, sortingOptionIndex: 0, }, + friends: {}, }, }, }, From f46af2cb1b3a96bb2ea31aa8d4e0adf0d8c03422 Mon Sep 17 00:00:00 2001 From: Yatharth Bhargava Date: Mon, 27 Mar 2023 20:12:16 -0400 Subject: [PATCH 5/7] make changes --- src/components/AppDataLoader/index.tsx | 6 +- src/components/InvitationModal/index.tsx | 177 ++----- .../InvitationModal/stylesheet.scss | 441 +++++++++--------- src/contexts/schedule.ts | 12 +- src/data/hooks/useFirebaseAuth.ts | 11 +- src/data/hooks/useVersionActions.ts | 26 +- 6 files changed, 294 insertions(+), 379 deletions(-) diff --git a/src/components/AppDataLoader/index.tsx b/src/components/AppDataLoader/index.tsx index b42bcfca..0647a48f 100644 --- a/src/components/AppDataLoader/index.tsx +++ b/src/components/AppDataLoader/index.tsx @@ -291,7 +291,7 @@ function ContextProvider({ deleteVersion, renameVersion, cloneVersion, - updateFriends, + deleteFriendRecord, } = useVersionActions({ updateTermScheduleData, setVersion, @@ -316,7 +316,7 @@ function ContextProvider({ setCurrentVersion: setVersion, addNewVersion, deleteVersion, - updateFriends, + deleteFriendRecord, renameVersion, cloneVersion, }, @@ -333,7 +333,7 @@ function ContextProvider({ updateSchedule, setVersion, addNewVersion, - updateFriends, + deleteFriendRecord, deleteVersion, renameVersion, cloneVersion, diff --git a/src/components/InvitationModal/index.tsx b/src/components/InvitationModal/index.tsx index 3eeb2d7e..d2b8ec42 100644 --- a/src/components/InvitationModal/index.tsx +++ b/src/components/InvitationModal/index.tsx @@ -2,9 +2,9 @@ import React, { ChangeEvent, KeyboardEvent, useCallback, - useMemo, useContext, useState, + useRef, } from 'react'; import { Tooltip as ReactTooltip } from 'react-tooltip'; import { faCircle, faClose } from '@fortawesome/free-solid-svg-icons'; @@ -25,135 +25,69 @@ import './stylesheet.scss'; * Inner content of the invitation modal. */ export function InvitationModalContent(): React.ReactElement { - const [{ currentFriends, currentVersion, term }, { updateFriends }] = + const [{ currentFriends, currentVersion, term }, { deleteFriendRecord }] = useContext(ScheduleContext); const accountContext = useContext(AccountContext); - const [input, setInput] = useState(''); + const input = useRef(null); const [validMessage, setValidMessage] = useState(''); const [validClassName, setValidClassName] = useState(''); - // Boolean to hide and open search dropdown - const [hidden, setHidden] = useState(true); - - // Array for testing dropdown of recent invites - // eslint-disable-next-line - const [recentInvites, setRecentInvites] = useState([ - 'user1@example.com', - 'user2@example.com', - ]); - const [activeIndex, setActiveIndex] = useState(-1); - const handleChangeSearch = useCallback((e: ChangeEvent) => { - let search = e.target.value.trim(); - const results = /^([A-Z]+)(\d.*)$/i.exec(search); - if (results != null) { - const [, email, number] = results as unknown as [string, string, string]; - search = `${email}${number}`; - } - setHidden(false); - setInput(search); setValidMessage(''); - setActiveIndex(-1); + setValidClassName(''); }, []); - const searchResults = useMemo(() => { - if (!input) return recentInvites; - const results = /^([A-Z]+) ?((\d.*)?)$/i.exec(input?.toUpperCase()); - if (!results) { - return []; + const handleKeyDown = useCallback((e: KeyboardEvent) => { + switch (e.key) { + case 'Enter': + verifyEmail(); + break; + default: + return; } - - return recentInvites.filter((invite) => { - const searchMatch = recentInvites.includes(invite); - return searchMatch; - }); - }, [input, recentInvites]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - switch (e.key) { - case 'ArrowDown': - setHidden(false); - setInput( - searchResults[ - Math.min(activeIndex + 1, searchResults.length - 1) - ] as string - ); - setActiveIndex(Math.min(activeIndex + 1, searchResults.length - 1)); - break; - case 'ArrowUp': - setHidden(false); - setInput(searchResults[Math.max(activeIndex - 1, 0)] as string); - setActiveIndex(Math.max(activeIndex - 1, 0)); - break; - case 'Escape': - setHidden(true); - break; - case 'Enter': - setHidden(true); - break; - default: - return; - } - e.preventDefault(); - }, - [searchResults, activeIndex] - ); - - const handleCloseDropdown = useCallback( - (index?: number) => { - if (index !== undefined) { - setInput(searchResults[index] as string); - setActiveIndex(index); - } - setHidden(true); - }, - [searchResults] - ); + e.preventDefault(); + }, []); const sendInvitation = async (): Promise => { const IdToken = await (accountContext as SignedIn).getToken(); - axios - .post( - 'http://127.0.0.1:5001/gt-scheduler-web-dev/us-central1/createFriendInvitation', - { - term, - friendEmail: input, - IDToken: IdToken, - version: currentVersion, - } - ) - .then(() => { - setValidMessage('Successfully sent!'); - setValidClassName('valid-email'); - }) - .catch((err) => { - setValidClassName('invalid-email'); - 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.'); - }); + return axios.post( + 'http://127.0.0.1:5001/gt-scheduler-web-dev/us-central1/createFriendInvitation', + { + term, + friendEmail: input.current?.value, + IDToken: IdToken, + version: currentVersion, + } + ); }; // verify email with a regex and send invitation if valid const verifyEmail = (): void => { + console.log(input.current?.value); if ( // eslint-disable-next-line - /^\S+@\S+\.\S+$/.test(input) + input.current && + /^\S+@\S+\.\S+$/.test(input.current.value) ) { sendInvitation() .then(() => { - setInput(''); + setValidMessage('Successfully sent!'); + setValidClassName('valid-email'); + if (input.current) { + input.current.value = ''; + } }) - .catch(() => { - setValidMessage('Error sending invitation. Please try again later.'); + .catch((err) => { + console.log(err); setValidClassName('invalid-email'); + 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.'); }); } else { setValidMessage('Invalid Email'); @@ -163,9 +97,7 @@ export function InvitationModalContent(): React.ReactElement { // delete friend from record of friends const handleDelete = (friendId: string): void => { - const newFriends = castDraft(currentFriends); - delete newFriends[friendId]; - updateFriends(currentVersion, newFriends); + deleteFriendRecord(currentVersion, friendId); }; return ( @@ -183,30 +115,14 @@ export function InvitationModalContent(): React.ReactElement { type="email" id="email" key="email" - value={input} + ref={input} className="email" placeholder="recipient@example.com" list="recent-invites" - onChange={handleChangeSearch} onFocus={handleChangeSearch} onKeyDown={handleKeyDown} - onBlur={(): void => handleCloseDropdown()} + onChange={handleChangeSearch} /> - {!hidden && ( -
- {searchResults.map((element, index) => ( -
handleCloseDropdown(index)} - > - {element} -
- ))} -
- )} {validMessage}