Completed MainPage

This commit is contained in:
Ceferino Patino 2022-03-12 17:46:57 -06:00
commit 461603ff3c
26 changed files with 532 additions and 372 deletions

View file

@ -32,7 +32,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>KnowItAll</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -1,27 +1,25 @@
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Header from './components/Header';
import Navbar from './components/Navbar';
import MainPage from './components/MainPage';
import Form from './components/Form';
import Login from './components/Login';
import Register from './components/Register';
import EditUser from './components/EditUser';
import Recover from './components/Recover';
import Reset from './components/Reset';
import Form from './components/utils/Form';
import Login from './components/forms/Login';
import Register from './components/forms/Register';
import EditUser from './components/forms/EditUser';
import Recover from './components/forms/Recover';
import Reset from './components/forms/Reset';
import JoinedOrgs from './components/JoinedOrgs';
import OrgDashboard from './components/OrgDashboard';
import NotFound from './components/NotFound';
import JoinedOrgs from './components/orgs/JoinedOrgs';
import OrgDashboard from './components/orgs/OrgDashboard';
function App() {
return (
<React.Fragment>
<BrowserRouter>
<Routes>
<Route path='' element={<Header />}>
<Route path='' element={<Navbar />}>
<Route index element={<MainPage />} />
<Route path='login' element={<Login />} />
@ -32,12 +30,13 @@ function App() {
<Route path=':id' element={<Reset />} />
</Route>
<Route path=''>
<Route path='org' element={<JoinedOrgs />} />
<Route path='joined'>
<Route index element={<JoinedOrgs />} />
<Route path=':id' element={<OrgDashboard />} />
</Route>
<Route path='*' element={<NotFound />} />
<Route path='*' element={<Navigate to='' />} />
</Route>
</Routes>
</BrowserRouter>

View file

@ -1,83 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import { Paper, Typography, Box, Stack } from '@mui/material';
import { useTheme } from '@mui/material/styles';
function OrgUnit({ org }) {
const { orgID, orgName, memberCount, createdAt } = org;
return (
<React.Fragment>
<Typography variant='h6'>
<Link to={orgID}>Name: {orgName}</Link>
</Typography>
<Box>
<Stack spacing={2} direction={{ xs: 'column', md: 'row' }}>
<Typography variant='body1'>
Members: {memberCount}
</Typography>
<Typography variant='body1'>
Created On: {createdAt}
</Typography>
</Stack>
</Box>
</React.Fragment>
);
}
function JoinedOrgs() {
const theme = useTheme();
const [orgs, setOrgs] = useState([]);
useEffect(() => {
async function getOrgs() {
axios
.get(
`${process.env.DOMAIN_ROOT}/auth/orgs?token=${Cookies.get(
'token'
)}`
)
.then((response) => {
setOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
});
}
getOrgs();
});
return (
<Paper
sx={{
...theme.typography.body2,
padding: '2vh',
color: theme.palette.text.secondary,
flexGrow: 1,
height: '89vh',
margin: '2vh',
}}
>
<Typography variant='h6'>Organizations</Typography>
<Box sx={{ overflowY: 'auto' }}>
{orgs.map((org) => (
<OrgUnit org={org} />
))}
</Box>
</Paper>
);
}
export default JoinedOrgs;

View file

@ -2,62 +2,184 @@ import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import { Paper, Button, Typography, Stack, Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
Button,
Typography,
Box,
Grid,
Stack,
TextField,
FormGroup,
} from '@mui/material';
import OrgUnit from './utils/OrgUnit';
import StackItem from './utils/StackItem';
function MainPage() {
const theme = useTheme();
const [org, setOrg] = useState({});
const [ownedOrgs, setOwnedOrgs] = useState([]);
const [orgs, setOrgs] = useState([]);
const [form, setForm] = useState({
name: '',
value: '',
});
const linkStyle = { textDecoration: 'none', color: 'inherit' };
useEffect(() => {
async function getOwnedOrgs() {
await axios
.get(
`${process.env.DOMAIN_ROOT}/auth?token=${Cookies.get(
'token'
)}`
)
.then((response) => {
setOwnedOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
});
}
getOwnedOrgs();
});
useEffect(() => {
async function getOrgs() {
await axios
.get(
`${process.env.DOMAIN_ROOT}/auth/orgs?token=${Cookies.get(
'token'
)}`
)
.then((response) => {
setOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
});
}
getOrgs();
});
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
return (
<React.Fragment>
<Paper
sx={{
...theme.typography.body2,
padding: '2vh',
color: theme.palette.text.secondary,
flexGrow: 1,
height: '30vh',
margin: '2vh',
overflow: 'auto',
}}
>
{org && (
<React.Fragment>
<Typography variant='h6'>
<Link to={`orgs/${org.orgID}`} style={linkStyle}>
Your Organization: {org.orgName}
</Link>
</Typography>
<Box margin={2}>
<Stack spacing={2}>
<Typography variant='body1'>
Members: {org.memberCount}
<Box sx={{ flexGrow: 1 }}>
<Grid
container
spacing={2}
alignItems='center'
style={{ height: '95vh' }}
>
<Grid item xs={1} sm={3} md={3.5} />
<Grid item xs={10} sm={6} md={5}>
<Stack spacing={2}>
<StackItem text='Join an Organization'>
<form>
<FormGroup row>
<TextField
id='standard-name'
label='Organization ID'
name='orgID'
value={form.orgID}
onChange={handleChange}
sx={{ width: '75%' }}
/>
<Button
type='submit'
variant='outlined'
color='primary'
disableElevation
sx={{ width: '25%' }}
>
Join Organization
</Button>
</FormGroup>
</form>
</StackItem>
<StackItem text='Create an Organization'>
<form>
<FormGroup row>
<TextField
id='standard-name'
label='Organization Name'
name='orgName'
value={form.orgName}
onChange={handleChange}
sx={{ width: '70%' }}
/>
<Button
type='submit'
variant='outlined'
color='primary'
disableElevation
sx={{ width: '30%' }}
>
Create Organization
</Button>
</FormGroup>
</form>
</StackItem>
<StackItem>
<Typography variant='h6'>
<Link to='org' style={linkStyle}>
Your Organizations
</Link>
</Typography>
<Typography variant='body1'>
Created On: {org.createdAt}
{ownedOrgs.length === 0 && (
<Typography variant='body2'>
You do own any organizations
</Typography>
)}
{ownedOrgs.map((org) => (
<OrgUnit org={org} />
))}
</StackItem>
<StackItem>
<Typography variant='h6'>
<Link to='joined' style={linkStyle}>
Joined Organizations
</Link>
</Typography>
</Stack>
</Box>
</React.Fragment>
)}
</Paper>
<Paper
sx={{
...theme.typography.body2,
padding: '2vh',
textAlign: 'center',
color: theme.palette.text.secondary,
flexGrow: 1,
height: '57vh',
margin: '2vh',
overflow: 'auto',
}}
></Paper>
{orgs.length === 0 && (
<Typography variant='body2'>
You are not a member in any
organizations
</Typography>
)}
{orgs.map((org) => (
<OrgUnit org={org} />
))}
</StackItem>
</Stack>
</Grid>
<Grid item xs={1} sm={3} md={3.5} />
</Grid>
</Box>
</React.Fragment>
);
}

View file

@ -15,7 +15,7 @@ import {
import MenuIcon from '@mui/icons-material/Menu';
import { ThemeProvider, createTheme } from '@mui/material/styles';
function Header() {
function Navbar() {
const navigate = useNavigate();
const [token, setToken] = useState('');
@ -28,7 +28,10 @@ function Header() {
useEffect(() => {
setToken(() => Cookies.get('token'));
}, [navigate]);
if (!token) {
navigate('login');
}
}, [navigate, token]);
async function handleLogout() {
await axios.post(`${process.env.DOMAIN_ROOT}/auth/logout`, {
@ -51,7 +54,9 @@ function Header() {
aria-label='menu'
sx={{ mr: 2 }}
>
<MenuIcon />
<Link to='/' style={linkStyle}>
<MenuIcon />
</Link>
</IconButton>
<Typography
variant='h6'
@ -90,4 +95,4 @@ function Header() {
);
}
export default Header;
export default Navbar;

View file

@ -1,15 +0,0 @@
import React from 'react';
import { Box } from '@mui/material';
function NotFound() {
return (
<React.Fragment>
<Box sx={{ flexGrow: 1 }}>
<p>404 Not Found</p>
</Box>
</React.Fragment>
);
}
export default NotFound;

View file

@ -1,75 +0,0 @@
import React, { useState } from 'react';
import axios from 'axios';
import { Box, Typography, TextField, Button } from '@mui/material';
function Recover() {
const [form, setForm] = useState({
email: '',
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.get(`${process.env.DOMAIN_ROOT}/auth/reset-password?${form.email}`)
.then((response) => {
setSuccess(() => response.data);
})
.catch((e) => setError(() => e.response.data));
}
return (
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Recover my Password
</Typography>
<TextField
required
fullWidth
id='outline-required'
label='Email'
name='email'
variant='outlined'
onChange={handleChange}
value={form.email}
sx={{ margin: '1vh 0vh' }}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='error'>
{error}
</Typography>
</Box>
)}
{success && (
<Box textAlign='right'>
<Typography variant='body2' color='primary'>
{success}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
>
Recover my password
</Button>
</form>
);
}
export default Recover;

View file

@ -1,89 +0,0 @@
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import axios from 'axios';
import { Box, Typography, TextField, Button } from '@mui/material';
function Reset() {
const navigate = useNavigate();
const { id } = useParams();
const [form, setForm] = useState({
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.patch(`${process.env.DOMAIN_ROOT}/auth/reset-password/${id}`, {
password: form.password,
})
.then(() => {
navigate('/login');
})
.catch((e) => setError(() => e.response.data));
}
return (
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Reset my Password
</Typography>
<TextField
required
fullWidth
id='outline-required'
label='Password'
name='password'
variant='outlined'
type='password'
onChange={handleChange}
value={form.password}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
id='outline-required'
label='Confirm Password'
name='confirmPassword'
variant='outlined'
type='password'
onChange={handleChange}
value={form.confirmPassword}
sx={{ margin: '1vh 0vh' }}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='error'>
{error}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
disabled={form.password !== form.confirmPassword}
>
Reset my Password
</Button>
</form>
);
}
export default Reset;

View file

@ -6,7 +6,7 @@ import Cookies from 'js-cookie';
import { Box, Typography, TextField, Button } from '@mui/material';
import Form from './Form';
import Form from '../utils/Form';
function EditUser() {
const navigate = useNavigate();
@ -39,9 +39,7 @@ function EditUser() {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
gender: user.gender,
pronouns: user.pronouns,
ethnicity: user.ethnicity,
};
});
});

View file

@ -13,7 +13,7 @@ import {
Checkbox,
} from '@mui/material';
import Form from './Form';
import Form from '../utils/Form';
function Login() {
const navigate = useNavigate();

View file

@ -0,0 +1,79 @@
import React, { useState } from 'react';
import axios from 'axios';
import { Box, Typography, TextField, Button } from '@mui/material';
import Form from '../utils/Form';
function Recover() {
const [form, setForm] = useState({
email: '',
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.get(`${process.env.DOMAIN_ROOT}/auth/reset-password?${form.email}`)
.then((response) => {
setSuccess(() => response.data);
})
.catch((e) => setError(() => e.response.data));
}
return (
<Form>
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Recover my Password
</Typography>
<TextField
required
fullWidth
id='outline-required'
label='Email'
name='email'
variant='outlined'
onChange={handleChange}
value={form.email}
sx={{ margin: '1vh 0vh' }}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='error'>
{error}
</Typography>
</Box>
)}
{success && (
<Box textAlign='right'>
<Typography variant='body2' color='primary'>
{success}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
>
Recover my password
</Button>
</form>
</Form>
);
}
export default Recover;

View file

@ -5,7 +5,7 @@ import axios from 'axios';
import { Box, Typography, TextField, Button } from '@mui/material';
import Form from './Form';
import Form from '../utils/Form';
function Register() {
const navigate = useNavigate();
@ -14,9 +14,7 @@ function Register() {
firstName: '',
lastName: '',
email: '',
gender: '',
pronouns: '',
ethnicity: '',
password: '',
confirmPassword: '',
});
@ -82,26 +80,6 @@ function Register() {
value={form.email}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
fullWidth
id='outline-required'
label='Ethnicity'
name='ethnicity'
variant='outlined'
onChange={handleChange}
value={form.ethnicity}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
fullWidth
id='outline-required'
label='Gender'
name='gender'
variant='outlined'
onChange={handleChange}
value={form.gender}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
fullWidth
id='outline-required'

View file

@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import axios from 'axios';
import { Box, Typography, TextField, Button } from '@mui/material';
import Form from '../utils/Form';
function Reset() {
const navigate = useNavigate();
const { id } = useParams();
const [form, setForm] = useState({
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.patch(`${process.env.DOMAIN_ROOT}/auth/reset-password/${id}`, {
password: form.password,
})
.then(() => {
navigate('/login');
})
.catch((e) => setError(() => e.response.data));
}
return (
<Form>
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Reset my Password
</Typography>
<TextField
required
fullWidth
id='outline-required'
label='Password'
name='password'
variant='outlined'
type='password'
onChange={handleChange}
value={form.password}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
id='outline-required'
label='Confirm Password'
name='confirmPassword'
variant='outlined'
type='password'
onChange={handleChange}
value={form.confirmPassword}
sx={{ margin: '1vh 0vh' }}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='error'>
{error}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
disabled={form.password !== form.confirmPassword}
>
Reset my Password
</Button>
</form>
</Form>
);
}
export default Reset;

View file

@ -0,0 +1,31 @@
import React from 'react';
import axios from 'axios';
import Cookies from 'js-cookie';
import Organizations from '../utils/Organizations';
function JoinedOrgs() {
async function getOrgs(setOrgs) {
axios
.get(
`${process.env.DOMAIN_ROOT}/auth?token=${Cookies.get('token')}`
)
.then((response) => {
setOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
});
}
return <Organizations retrieveOrgs={getOrgs} />;
}
export default JoinedOrgs;

View file

@ -0,0 +1,33 @@
import React from 'react';
import axios from 'axios';
import Cookies from 'js-cookie';
import Organizations from '../utils/Organizations';
function JoinedOrgs() {
async function getOrgs(setOrgs) {
axios
.get(
`${process.env.DOMAIN_ROOT}/auth/orgs?token=${Cookies.get(
'token'
)}`
)
.then((response) => {
setOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
});
}
return <Organizations retrieveOrgs={getOrgs} />;
}
export default JoinedOrgs;

View file

@ -13,7 +13,7 @@ import {
Button,
} from '@mui/material';
import Dashboard from './Dashboard';
import Dashboard from '../utils/Dashboard';
function OrgDashboard() {
const { orgID } = useParams();
@ -30,6 +30,7 @@ function OrgDashboard() {
orgMembers: [],
createdAt: '',
});
const [rows, setRows] = useState([]);
useEffect(() => {

View file

@ -0,0 +1,28 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Stack, Box, Typography } from '@mui/material';
function OrgUnit({ org }) {
const { orgID, orgName, memberCount, createdAt } = org;
return (
<React.Fragment>
<Typography variant='h6'>
<Link to={orgID}>Name: {orgName}</Link>
</Typography>
<Box>
<Stack spacing={2} direction={{ xs: 'column', md: 'row' }}>
<Typography variant='body1'>
Members: {memberCount}
</Typography>
<Typography variant='body1'>
Created On: {createdAt}
</Typography>
</Stack>
</Box>
</React.Fragment>
);
}
export default OrgUnit;

View file

@ -0,0 +1,38 @@
import React, { useEffect, useState } from 'react';
import { Paper, Typography, Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import OrgUnit from './OrgUnit';
function Organizations({ retrieveOrgs }) {
const theme = useTheme();
const [orgs, setOrgs] = useState([]);
useEffect(() => {
retrieveOrgs(setOrgs);
}, [retrieveOrgs]);
return (
<Paper
sx={{
...theme.typography.body2,
padding: '2vh',
color: theme.palette.text.secondary,
flexGrow: 1,
height: '89vh',
margin: '2vh',
}}
>
<Typography variant='h6'>Organizations</Typography>
<Box sx={{ overflowY: 'auto' }}>
{orgs.map((org) => (
<OrgUnit org={org} />
))}
</Box>
</Paper>
);
}
export default Organizations;

View file

@ -0,0 +1,22 @@
import React from 'react';
import { Paper, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
function StackItem({ text, children }) {
const theme = useTheme();
const paperStyle = {
...theme.typography.body2,
padding: '2vh',
color: theme.palette.text.secondary,
};
return (
<Paper sx={paperStyle}>
<Typography variant='h6'>{text}</Typography>
{children}
</Paper>
);
}
export default StackItem;

View file

@ -11,8 +11,8 @@ module.exports = (sequelize, DataTypes) => {
foreignKey: 'userID',
onDelete: 'CASCADE',
});
User.hasOne(models.Organization, {
as: 'ownedOrg',
User.hasMany(models.Organization, {
as: 'ownedOrgs',
foreignKey: 'ownerID',
onDelete: 'CASCADE',
});
@ -52,12 +52,6 @@ module.exports = (sequelize, DataTypes) => {
pronouns: {
type: DataTypes.STRING,
},
gender: {
type: DataTypes.STRING,
},
ethnicity: {
type: DataTypes.STRING,
},
},
{ sequelize }
);

View file

@ -1,8 +1,8 @@
async function GetKnownOrgs(req, res, next) {
async function GetMemberOrgs(req, res, next) {
const user = req.user;
const result = await user.getMemberOrgs();
return res.send(result);
}
module.exports = GetKnownOrgs;
module.exports = GetMemberOrgs;

View file

@ -1,7 +1,7 @@
async function GetOwnedGroups(req, res, next) {
const user = req.user;
const result = await user.getOwnedGroups();
const result = await user.getOwnedOrgs();
return res.send(result);
}

View file

@ -13,7 +13,7 @@ const RequestReset = require('./auth/RequestReset');
const ResetPassword = require('./auth/ResetPassword');
const GetUserDetails = require('./auth/GetUserDetails');
const GetKnownOrgs = require('./auth/GetKnownOrgs');
const GetOwnedGroups = require('./auth/GetOwnedGroups');
const GetOwnedORgs = require('./auth/GetOwnedOrgs');
router.post('/register', CreateUser);
router.patch('/update', tokenAuth, UpdateUserDetails);
@ -23,7 +23,8 @@ router.get('/reset-password', RequestReset);
router.patch('/reset-password/:id', ResetPassword);
router.get('/:userID', tokenAuth, checkKnownUser, GetUserDetails);
router.get('/', tokenAuth, GetOwnedOrgs);
router.get('/orgs', tokenAuth, GetKnownOrgs);
router.get('/groups', tokenAuth, GetOwnedGroups);
module.exports = router;

View file

@ -6,12 +6,12 @@ async function CreateOrg(req, res, next) {
return res.status(400).send('Form missing necessary fields');
}
const existing = await user.getOwnedOrg();
if (existing) {
const existing = await user.getOwnedOrgs();
if (existing.map((org) => org.orgName).includes(orgName)) {
return res
.status(500)
.send(
'This user already owns an organization, delete it to create another.'
'This user already has an organization with this name, please pick another.'
);
}