Skip to content

Commit

Permalink
Merge pull request #29 from hack4impact-upenn/ben-fall22-updates
Browse files Browse the repository at this point in the history
Alerts, Invites, and Heroku Deploy Config
  • Loading branch information
vszammit authored Jan 24, 2023
2 parents 642000b + c133ff2 commit f1895b9
Show file tree
Hide file tree
Showing 25 changed files with 838 additions and 28 deletions.
20 changes: 10 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
yarn-error.log
/node_modules

.env
/.env

.env.local
.env.development.local
.env.test.local
.env.production.local
yarn-error.log
/node_modules
/bin
.env
/.env

.env.local
.env.development.local
.env.test.local
.env.production.local
14 changes: 7 additions & 7 deletions client/src/AdminDashboard/AdminDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Typography, Grid } from '@mui/material';
import ScreenGrid from '../components/ScreenGrid';
import UserTable from './UserTable';
import InviteUserButton from '../components/buttons/InviteUserButton';

/**
* A page only accessible to admins that displays all users in a table and allows
Expand All @@ -10,14 +11,13 @@ import UserTable from './UserTable';
function AdminDashboardPage() {
return (
<ScreenGrid>
<Grid
item
direction="column"
justifyContent="flex-start"
alignItems="stretch"
>
<Grid item>
<Typography variant="h2">Welcome to the Admin Dashboard</Typography>

</Grid>
<Grid item container width="60vw" justifyContent="flex-end">
<InviteUserButton />
</Grid>
<Grid item>
<div style={{ height: '60vh', width: '60vw' }}>
<UserTable />
</div>
Expand Down
4 changes: 4 additions & 0 deletions client/src/AdminDashboard/DeleteUserButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Button from '@mui/material/Button';
import { deleteUser } from './api';
import LoadingButton from '../components/buttons/LoadingButton';
import ConfirmationModal from '../components/ConfirmationModal';
import AlertType from '../util/types/alert';
import useAlert from '../util/hooks/useAlert';

interface DeleteUserButtonProps {
admin: boolean;
Expand All @@ -19,11 +21,13 @@ interface DeleteUserButtonProps {
* function is called upon successfully deletion of user from the database.
*/
function DeleteUserButton({ admin, email, removeRow }: DeleteUserButtonProps) {
const { setAlert } = useAlert();
const [isLoading, setLoading] = useState(false);
async function handleDelete() {
setLoading(true);
if (await deleteUser(email)) {
removeRow(email);
setAlert(`User ${email} has been deleted.`, AlertType.SUCCESS);
} else {
setLoading(false);
}
Expand Down
7 changes: 7 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import RegisterPage from './Authentication/RegisterPage';
import LoginPage from './Authentication/LoginPage';
import EmailResetPasswordPage from './Authentication/EmailResetPasswordPage';
import ResetPasswordPage from './Authentication/ResetPasswordPage';
import AlertPopup from './components/AlertPopup';
import InviteRegisterPage from './Authentication/InviteRegisterPage';

function App() {
return (
Expand All @@ -29,6 +31,7 @@ function App() {
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider theme={theme}>
<CssBaseline>
<AlertPopup />
<Routes>
{/* Routes accessed only if user is not authenticated */}
<Route element={<UnauthenticatedRoutesWrapper />}>
Expand All @@ -47,6 +50,10 @@ function App() {
element={<ResetPasswordPage />}
/>
</Route>
<Route
path="/invite/:token"
element={<InviteRegisterPage />}
/>
{/* Routes accessed only if user is authenticated */}
<Route element={<ProtectedRoutesWrapper />}>
<Route path="/home" element={<HomePage />} />
Expand Down
283 changes: 283 additions & 0 deletions client/src/Authentication/InviteRegisterPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
import React, { useEffect, useState } from 'react';
import { Link, TextField, Grid, Typography } from '@mui/material';
import { useNavigate, Link as RouterLink, useParams } from 'react-router-dom';
import FormCol from '../components/form/FormCol';
import {
emailRegex,
InputErrorMessage,
nameRegex,
passwordRegex,
} from '../util/inputvalidation';
import { registerInvite } from './api';
import AlertDialog from '../components/AlertDialog';
import PrimaryButton from '../components/buttons/PrimaryButton';
import ScreenGrid from '../components/ScreenGrid';
import FormRow from '../components/form/FormRow';
import FormGrid from '../components/form/FormGrid';
import { useData } from '../util/api';

/**
* A page users visit to be able to register for a new account by inputting
* fields such as their name, email, and password.
*/
function InviteRegisterPage() {
const { token } = useParams();
const navigate = useNavigate();

// Default values for state
const defaultValues = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
};
const defaultShowErrors = {
firstName: false,
lastName: false,
email: false,
password: false,
confirmPassword: false,
alert: false,
};
const defaultErrorMessages = {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
alert: '',
};
type ValueType = keyof typeof values;

// State values and hooks
const [values, setValueState] = useState(defaultValues);
const [showError, setShowErrorState] = useState(defaultShowErrors);
const [errorMessage, setErrorMessageState] = useState(defaultErrorMessages);
const [alertTitle, setAlertTitle] = useState('Error');
const [isRegistered, setRegistered] = useState(false);
const [validToken, setValidToken] = useState(true);
const [email, setEmail] = useState('');

// Helper functions for changing only one field in a state object
const setValue = (field: string, value: string) => {
setValueState((prevState) => ({
...prevState,
...{ [field]: value },
}));
};
const setShowError = (field: string, show: boolean) => {
setShowErrorState((prevState) => ({
...prevState,
...{ [field]: show },
}));
};
const setErrorMessage = (field: string, msg: string) => {
setErrorMessageState((prevState) => ({
...prevState,
...{ [field]: msg },
}));
};
const invite = useData(`admin/invite/${token}`);
useEffect(() => {
if (!invite?.data && invite !== null) {
setValidToken(false);
} else {
setEmail(invite?.data?.email);
setValue('email', invite?.data?.email);
}
}, [invite]);

const handleAlertClose = () => {
if (isRegistered) {
navigate('/login');
}
setShowError('alert', false);
};

const clearErrorMessages = () => {
setShowErrorState(defaultShowErrors);
setErrorMessageState(defaultErrorMessages);
};

const validateInputs = () => {
clearErrorMessages();
let isValid = true;

// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const valueTypeString in values) {
const valueType = valueTypeString as ValueType;
if (!values[valueType]) {
setErrorMessage(valueTypeString, InputErrorMessage.MISSING_INPUT);
setShowError(valueTypeString, true);
isValid = false;
}
}

if (!values.firstName.match(nameRegex)) {
setErrorMessage('firstName', InputErrorMessage.INVALID_NAME);
setShowError('firstName', true);
isValid = false;
}
if (!values.lastName.match(nameRegex)) {
setErrorMessage('lastName', InputErrorMessage.INVALID_NAME);
setShowError('lastName', true);
isValid = false;
}
if (!values.email.match(emailRegex)) {
setErrorMessage('email', InputErrorMessage.INVALID_EMAIL);
setShowError('email', true);
isValid = false;
}
if (!values.password.match(passwordRegex)) {
setErrorMessage('password', InputErrorMessage.INVALID_PASSWORD);
setShowError('password', true);
isValid = false;
}
if (!(values.confirmPassword === values.password)) {
setErrorMessage('confirmPassword', InputErrorMessage.PASSWORD_MISMATCH);
setShowError('confirmPassword', true);
isValid = false;
}

return isValid;
};

async function handleSubmit() {
if (validateInputs() && token) {
registerInvite(
values.firstName,
values.lastName,
values.email,
values.password,
token,
)
.then(() => {
setShowError('alert', true);
setAlertTitle('');
setRegistered(true);
setErrorMessage('alert', 'Account created, please log in');
})
.catch((e) => {
setShowError('alert', true);
setErrorMessage('alert', e.message);
});
}
}

const title = "Let's get started";
if (!validToken) {
return (
<ScreenGrid>
<Typography variant="h2">Invalid Invite Token</Typography>
</ScreenGrid>
);
}
return (
<ScreenGrid>
<FormGrid>
<FormCol>
<Grid item container justifyContent="center">
<Typography variant="h2">{title}</Typography>
</Grid>
<Grid item width="1">
<TextField
fullWidth
size="small"
type="text"
required
label="Email"
value={email}
disabled
/>
</Grid>
<FormRow>
<Grid item width=".5">
<TextField
fullWidth
error={showError.firstName}
helperText={errorMessage.firstName}
size="small"
type="text"
required
label="First Name"
value={values.firstName}
onChange={(e) => setValue('firstName', e.target.value)}
/>
</Grid>
<Grid item width=".5">
<TextField
fullWidth
error={showError.lastName}
helperText={errorMessage.lastName}
size="small"
type="text"
required
label="Last Name"
value={values.lastName}
onChange={(e) => setValue('lastName', e.target.value)}
/>
</Grid>
</FormRow>
<FormRow>
<Grid item width=".5">
<TextField
fullWidth
error={showError.password}
helperText={errorMessage.password}
size="small"
type="password"
required
label="Password"
value={values.password}
onChange={(e) => setValue('password', e.target.value)}
/>
</Grid>
<Grid item container width=".5">
<TextField
fullWidth
error={showError.confirmPassword}
helperText={errorMessage.confirmPassword}
size="small"
type="password"
required
label=" Confirm Password"
value={values.confirmPassword}
onChange={(e) => setValue('confirmPassword', e.target.value)}
/>
</Grid>
</FormRow>
<Grid item container justifyContent="center">
<PrimaryButton
fullWidth
type="submit"
variant="contained"
color="primary"
onClick={() => handleSubmit()}
>
Register
</PrimaryButton>
</Grid>
<FormRow>
<Grid container justifyContent="center">
<Link component={RouterLink} to="../">
Back to Login
</Link>
</Grid>
</FormRow>
</FormCol>
{/* The alert that pops up */}
<Grid item>
<AlertDialog
showAlert={showError.alert}
title={alertTitle}
message={errorMessage.alert}
onClose={handleAlertClose}
/>
</Grid>
</FormGrid>
</ScreenGrid>
);
}

export default InviteRegisterPage;
Loading

0 comments on commit f1895b9

Please sign in to comment.