Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Functionality to Invitation Modal #186

Open
wants to merge 8 commits into
base: bog-changes-s23
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/components/AppDataLoader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScheduleContextValue>(
Expand All @@ -301,6 +306,7 @@ function ContextProvider({
oscar,
currentVersion,
allVersionNames,
currentFriends: scheduleVersion.friends,
...castDraft(scheduleVersion.schedule),
},
{
Expand All @@ -310,6 +316,7 @@ function ContextProvider({
setCurrentVersion: setVersion,
addNewVersion,
deleteVersion,
updateFriends,
yatharth-b marked this conversation as resolved.
Show resolved Hide resolved
renameVersion,
cloneVersion,
},
Expand All @@ -319,12 +326,14 @@ function ContextProvider({
oscar,
currentVersion,
allVersionNames,
scheduleVersion.friends,
scheduleVersion.schedule,
setTerm,
patchSchedule,
updateSchedule,
setVersion,
addNewVersion,
updateFriends,
deleteVersion,
renameVersion,
cloneVersion,
Expand Down
1 change: 0 additions & 1 deletion src/components/AppDataLoader/stages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ export function StageLoadRawScheduleDataFromFirebase({
children,
}: StageLoadRawScheduleDataFromFirebaseProps): React.ReactElement {
const loadingState = useRawScheduleDataFromFirebase(accountState);

if (loadingState.type !== 'loaded') {
return (
<AppSkeleton {...skeletonProps}>
Expand Down
16 changes: 9 additions & 7 deletions src/components/HeaderActionBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
109 changes: 76 additions & 33 deletions src/components/InvitationModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,32 @@ 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 { ApiErrorResponse } from '../../data/types';
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';

/**
* 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([
['[email protected]', 'Pending'],
['[email protected]', 'Accepted'],
['[email protected]', 'Accepted'],
['[email protected]', 'Accepted'],
['[email protected]', 'Pending'],
['[email protected]', 'Accepted'],
['[email protected]', 'Accepted'],
['[email protected]', 'Accepted'],
]);
const [{ currentFriends, currentVersion, term }, { updateFriends }] =
useContext(ScheduleContext);
const accountContext = useContext(AccountContext);

// Array to test invalid email
const validUsers = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]',
];
const [input, setInput] = useState('');
const [validMessage, setValidMessage] = useState('');
const [validClassName, setValidClassName] = useState('');
Expand Down Expand Up @@ -122,16 +112,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<void> => {
const IdToken = await (accountContext as SignedIn).getToken();
yatharth-b marked this conversation as resolved.
Show resolved Hide resolved
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.');
});
};

// verify email with a regex and send invitation if valid
const verifyEmail = (): void => {
if (
// eslint-disable-next-line
/^\S+@\S+\.\S+$/.test(input)
) {
sendInvitation()
.then(() => {
setInput('');
})
.catch(() => {
setValidMessage('Error sending invitation. Please try again later.');
setValidClassName('invalid-email');
});
yatharth-b marked this conversation as resolved.
Show resolved Hide resolved
} 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);
};
yatharth-b marked this conversation as resolved.
Show resolved Hide resolved

return (
<div className="invitation-modal-content">
Expand Down Expand Up @@ -174,7 +209,7 @@ export function InvitationModalContent(): React.ReactElement {
)}
<text className={validClassName}>{validMessage}</text>
</div>
<button type="button" className="send-button" onClick={verifyUser}>
<button type="button" className="send-button" onClick={verifyEmail}>
Send Invite
</button>
</div>
Expand All @@ -185,22 +220,30 @@ export function InvitationModalContent(): React.ReactElement {
Users Invited to View <strong>Primary</strong>
</p>
<div className="shared-emails" key="email">
{emails.map((element) => (
<div className="email-and-status" id={element[0]}>
<div className="individual-shared-email" id={element[1]}>
{element[0]}
<Button className="button-remove">
{Object.keys(currentFriends).map((friend) => (
yatharth-b marked this conversation as resolved.
Show resolved Hide resolved
<div
className="email-and-status"
id={currentFriends[friend]?.email}
>
<div className="individual-shared-email">
{currentFriends[friend]?.email}
<Button
className="button-remove"
onClick={(): void => {
handleDelete(friend);
}}
>
<FontAwesomeIcon className="circle" icon={faCircle} />
<FontAwesomeIcon className="remove" icon={faClose} />
</Button>
<ReactTooltip
anchorId={element[0]}
anchorId={currentFriends[friend]?.email}
className="status-tooltip"
variant="dark"
place="top"
offset={2}
>
Status: {element[1]}
Status: {currentFriends[friend]?.status}
</ReactTooltip>
yatharth-b marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
Expand Down
17 changes: 9 additions & 8 deletions src/components/InviteBackLink/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +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<void> =>
// 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',
},
}
`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();
Expand Down
1 change: 1 addition & 0 deletions src/contexts/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type SignedOut = {
export type SignedIn = {
type: 'signedIn';
signOut: () => void;
getToken: () => Promise<string | void>;
name: string | null;
provider: string | null;
email: string | null;
Expand Down
20 changes: 19 additions & 1 deletion src/contexts/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, FriendShareData>;
allVersionNames: { id: string; name: string }[];
// `oscar` is included below as a separate type
};
Expand All @@ -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<string, FriendShareData>
) => void;
};
export type ScheduleContextValue = [
ScheduleContextData,
Expand All @@ -37,6 +42,7 @@ export const ScheduleContext = React.createContext<ScheduleContextValue>([
{
term: '',
currentVersion: '',
currentFriends: {},
allVersionNames: [],
oscar: EMPTY_OSCAR,
...defaultSchedule,
Expand Down Expand Up @@ -71,6 +77,18 @@ export const ScheduleContext = React.createContext<ScheduleContextValue>([
},
});
},
updateFriends: (
versionId: string,
newFriends: Record<string, FriendShareData>
): 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',
Expand Down
1 change: 1 addition & 0 deletions src/data/hooks/useExtractScheduleVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export default function useExtractScheduleVersion({
const id = generateScheduleVersionId();
draft.versions[id] = {
name: 'Primary',
friends: {},
createdAt: new Date().toISOString(),
schedule: castDraft(defaultSchedule),
};
Expand Down
18 changes: 18 additions & 0 deletions src/data/hooks/useFirebaseAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ export default function useFirebaseAuth(): LoadingState<AccountContextValue> {
name: user.displayName,
email: user.email,
id: user.uid,
getToken: (): Promise<string | void> => {
const { currentUser } = firebase.auth();
if (!currentUser) {
return Promise.reject(
new ErrorWithFields({
message: 'firebase.auth().currentUser is null',
})
);
}
return currentUser.getIdToken().catch((err) => {
softError(
new ErrorWithFields({
message: 'call to firebase.auth().getIdToken() failed',
source: err,
})
);
});
},
provider,
signOut: () => {
firebase
Expand Down
2 changes: 2 additions & 0 deletions src/data/hooks/useMigrateScheduleData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -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: [
Expand Down
Loading