diff --git a/client/src/App.jsx b/client/src/App.jsx index baed3cc..a45a0cc 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -2,19 +2,19 @@ import React, { useState } from 'react'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import Navbar from './components/Navbar'; -import MainPage from './components/MainPage'; +import MainPage from './controllers/MainPage'; -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 Form from './components/Form'; +import Login from './controllers/Login'; +import Register from './controllers/Register'; +import EditUser from './controllers/EditUser'; +import Recover from './controllers/Recover'; +import Reset from './controllers/Reset'; -import OwnedOrgs from './components/orgs/OwnedOrgs'; -import MemberOrgs from './components/orgs/MemberOrgs'; -import OrgDashboard from './components/orgs/OrgDashboard'; -import UpdateOrganization from './components/forms/UpdateOrganization'; +import OwnedOrgs from './controllers/OwnedOrgs'; +import MemberOrgs from './controllers/MemberOrgs'; +import OrgDashboard from './controllers/OrgDashboard'; +import UpdateOrganization from './controllers/UpdateOrganization'; function App() { const [token, setToken] = useState(true); diff --git a/client/src/components/Dashboard.jsx b/client/src/components/Dashboard.jsx new file mode 100644 index 0000000..a7821f0 --- /dev/null +++ b/client/src/components/Dashboard.jsx @@ -0,0 +1,221 @@ +import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + ButtonGroup, + Collapse, + Grid, + IconButton, + Stack, + Typography, + useMediaQuery, +} from '@mui/material'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import { DataGrid } from '@mui/x-data-grid'; + +import MemberUnit from './MemberUnit'; +import { DynamicPaper, DynamicStack } from './StyledElements'; + +function Dashboard({ + rows, + setSelection, + onClick, + org, + status, + handleDelete, + copyID, + copyJoinLink, + removeSelected, + leaveOrg, +}) { + const xs = useMediaQuery((theme) => theme.breakpoints.only('xs')); + const sm = useMediaQuery((theme) => theme.breakpoints.only('sm')); + const md = useMediaQuery((theme) => theme.breakpoints.only('md')); + + const [open, setOpen] = useState(false); + + const linkStyle = { textDecoration: 'none', color: 'inherit' }; + const columns = [ + { field: 'id', headerName: 'ID', flex: 1 }, + { field: 'firstName', headerName: 'First Name', flex: 3 }, + { field: 'lastName', headerName: 'Last Name', flex: 3 }, + { field: 'nickname', headerName: 'Nickname', flex: 2 }, + { + field: 'namePronounciation', + headerName: 'Pronounciation', + flex: 2, + sortable: false, + renderCell: (params) => ( + onClick(params.id)} + > + + + ), + }, + { field: 'pronouns', headerName: 'Pronouns', flex: 2 }, + { field: 'email', headerName: 'Email', flex: 10, sortable: false }, + ]; + + function handleOpen() { + setOpen((initial) => (initial ? false : true)); + } + + const buttonGroupOrientation = () => { + if (xs) return 'vertical'; + }; + + function calculateCollapsedSize() { + if (xs) return '37vh'; + if (sm) return '63vh'; + if (md) return '68vh'; + } + + function calculateTableHeight() { + return open ? '64vh' : '100%'; + } + + return ( + + + + }> + + Organizations/{org.name} + + + + + + + + + Organization ID: {org.orgID} + + + Members: {org.memberCount} + + + Created On: {org.createdAt} + + + + + + + Owner:{' '} + {`${org.owner.firstName} ${org.owner.lastName}`} + + + Email: {org.owner.email} + + + + + + {status ? ( + + + + + + + + + + + + ) : ( + + )} + + + + + + + + + + {(xs || sm) && + rows.map((member) => { + return ( + + ); + })} + {md && ( + + { + setSelection(() => newSelection.rows); + }} + checkboxSelection + /> + + )} + + + + ); +} + +export default Dashboard; diff --git a/client/src/components/EditUserForm.jsx b/client/src/components/EditUserForm.jsx new file mode 100644 index 0000000..5cf0c3a --- /dev/null +++ b/client/src/components/EditUserForm.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Box, Typography } from '@mui/material'; + +import Form from './Form'; +import RecordMp3 from './RecordMp3'; +import { FormSubmit, FormTextField } from './StyledElements'; + +function EditUserForm({ form, error, handleSubmit, handleChange }) { + const linkStyle = { textDecoration: 'none', color: 'inherit' }; + + return ( +
+ + + + + + {error && ( + + + {error} + + + )} + + Update Details + + + + Reset Your Password + + + + + + ); +} + +export default EditUserForm; diff --git a/client/src/components/Form.jsx b/client/src/components/Form.jsx new file mode 100644 index 0000000..50d4e1d --- /dev/null +++ b/client/src/components/Form.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import { Box, Grid, Paper, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + +function Form({ children, text }) { + const theme = useTheme(); + + return ( + + + + + + {text && ( + + {text} + + )} + {children} + + + + + + + ); +} + +export default Form; diff --git a/client/src/components/LoginForm.jsx b/client/src/components/LoginForm.jsx new file mode 100644 index 0000000..abe93f7 --- /dev/null +++ b/client/src/components/LoginForm.jsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Box, Checkbox, FormControlLabel, Typography } from '@mui/material'; + +import Form from '../Form'; +import { FormSubmit, FormTextField } from '../StyledElements'; + +function Login({ + form, + error, + handleSubmit, + handleChange, + handleCheckboxChange, +}) { + return ( +
+ + + Login + + + +
+ } + labelPlacement='start' + label='Remember me' + name='remember' + onChange={handleCheckboxChange} + /> +
+ {error && ( + + + {error} + + + )} + + Login + + + Dont have an account? Register + + + Forgot your password?{' '} + Recover Password + + + + ); +} + +export default Login; diff --git a/client/src/components/MainPage.jsx b/client/src/components/MainPage.jsx deleted file mode 100644 index 3bafc82..0000000 --- a/client/src/components/MainPage.jsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; - -import axios from 'axios'; -import Cookies from 'js-cookie'; - -import { - Button, - Typography, - Box, - Grid, - Stack, - TextField, - FormGroup, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; - -import OrgUnit from './utils/OrgUnit'; -import StackItem from './utils/StackItem'; - -const Form = styled(FormGroup)(({ theme }) => ({ - [theme.breakpoints.only('xs')]: { - column: true, - }, -})); -const FormField = styled(TextField)(({ theme }) => ({ - [theme.breakpoints.only('xs')]: { - column: true, - width: '75%', - }, -})); -const FormButton = styled(Button)(({ theme }) => ({ - disableElevation: true, - color: theme.palette.primary.main, - [theme.breakpoints.only('xs')]: { - column: true, - width: '25%', - }, -})); - -function MainPage() { - const navigate = useNavigate(); - - const [ownedOrgs, setOwnedOrgs] = useState([]); - const [orgs, setOrgs] = useState([]); - const [form, setForm] = useState({ - name: '', - id: '', - }); - - const linkStyle = { textDecoration: 'none', color: 'inherit' }; - - useEffect(() => { - if (Cookies.get('token')) { - getMemberOrgs(); - getOwnedOrgs(); - } else { - navigate('/login'); - } - }, [navigate]); - - function handleChange(e) { - const name = e.target.name; - const value = e.target.value; - - setForm((form) => { - return { ...form, [name]: value }; - }); - } - - async function getOwnedOrgs() { - await axios - .get( - `${process.env.REACT_APP_API_ROOT}/auth?token=${Cookies.get( - 'token' - )}` - ) - .then((response) => { - setOwnedOrgs(() => response.data); - }) - .catch((e) => { - console.log(e); - }); - } - - async function getMemberOrgs() { - await axios - .get( - `${ - process.env.REACT_APP_API_ROOT - }/auth/orgs?token=${Cookies.get('token')}` - ) - .then((response) => { - setOrgs(() => response.data); - }); - } - - async function createOrg(e) { - e.preventDefault(); - console.log('create'); - - await axios - .post(`${process.env.REACT_APP_API_ROOT}/org/create`, { - token: Cookies.get('token'), - orgName: form.name, - }) - .then(() => { - getOwnedOrgs(); - }); - } - - async function joinOrg(e) { - e.preventDefault(); - - await axios - .post(`${process.env.REACT_APP_API_ROOT}/org/${form.id}/add`, { - token: Cookies.get('token'), - }) - .then(() => { - getMemberOrgs(); - }); - } - - return ( - - - - - - - -
- - - - Join Organization - - - -
- -
- - - - Create Organization - - - -
- - - - Your Organizations - - - {ownedOrgs.length === 0 && ( - - You do own any organizations - - )} - - {ownedOrgs.map((org) => ( - - ))} - - - - - Joined Organizations - - - {orgs.length === 0 && ( - - You are not a member in any - organizations - - )} - - {orgs.map((org) => ( - - ))} - -
-
- - -
-
- ); -} - -export default MainPage; diff --git a/client/src/components/MainPageBase.jsx b/client/src/components/MainPageBase.jsx new file mode 100644 index 0000000..9f7c97c --- /dev/null +++ b/client/src/components/MainPageBase.jsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Box, Grid, Stack, Typography } from '@mui/material'; + +import OrgUnit from './OrgUnit'; +import StackItem from './StackItem'; +import { Form, FormButton, FormField } from './StyledElements'; + +function MainPageBase({ + form, + orgs, + ownedOrgs, + joinOrg, + createOrg, + handleChange, +}) { + const linkStyle = { textDecoration: 'none', color: 'inherit' }; + + return ( + + + + + + +
+ + + + Join Organization + + + +
+ +
+ + + + Create Organization + + + +
+ + + + Your Organizations + + + {ownedOrgs.length === 0 && ( + + You do own any organizations + + )} + + {ownedOrgs.map((org) => ( + + ))} + + + + + Joined Organizations + + + {orgs.length === 0 && ( + + You are not a member in any organizations + + )} + + {orgs.map((org) => ( + + ))} + +
+
+ + +
+ ); +} + +export default MainPageBase; diff --git a/client/src/components/utils/MemberUnit.jsx b/client/src/components/MemberUnit.jsx similarity index 99% rename from client/src/components/utils/MemberUnit.jsx rename to client/src/components/MemberUnit.jsx index 8f0ab0b..61b5e8d 100644 --- a/client/src/components/utils/MemberUnit.jsx +++ b/client/src/components/MemberUnit.jsx @@ -1,12 +1,11 @@ import React from 'react'; - import { Accordion, - AccordionSummary, AccordionDetails, - Typography, - IconButton, + AccordionSummary, Grid, + IconButton, + Typography, } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx index 7b2ad64..aac79dc 100644 --- a/client/src/components/Navbar.jsx +++ b/client/src/components/Navbar.jsx @@ -1,17 +1,16 @@ import React from 'react'; import { Link, Outlet } from 'react-router-dom'; - import { AppBar, Button, + CssBaseline, + IconButton, Toolbar, Typography, - IconButton, - CssBaseline, useMediaQuery, } from '@mui/material'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; import MenuIcon from '@mui/icons-material/Menu'; -import { ThemeProvider, createTheme } from '@mui/material/styles'; function Navbar({ token }) { const linkStyle = { textDecoration: 'none', color: 'inherit' }; diff --git a/client/src/components/utils/OrgUnit.jsx b/client/src/components/OrgUnit.jsx similarity index 99% rename from client/src/components/utils/OrgUnit.jsx rename to client/src/components/OrgUnit.jsx index b869482..e053c5e 100644 --- a/client/src/components/utils/OrgUnit.jsx +++ b/client/src/components/OrgUnit.jsx @@ -1,11 +1,10 @@ import React from 'react'; import { Link } from 'react-router-dom'; - import { - Typography, Accordion, - AccordionSummary, AccordionDetails, + AccordionSummary, + Typography, } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; diff --git a/client/src/components/utils/Organizations.jsx b/client/src/components/Organizations.jsx similarity index 72% rename from client/src/components/utils/Organizations.jsx rename to client/src/components/Organizations.jsx index 1371129..66fe020 100644 --- a/client/src/components/utils/Organizations.jsx +++ b/client/src/components/Organizations.jsx @@ -1,19 +1,12 @@ -import React, { useEffect, useState } from 'react'; - -import { Paper, Typography, Box } from '@mui/material'; +import React from 'react'; +import { Box, Paper, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import OrgUnit from './OrgUnit'; -function Organizations({ retrieveOrgs }) { +function Organizations({ orgs }) { const theme = useTheme(); - const [orgs, setOrgs] = useState([]); - - useEffect(() => { - retrieveOrgs(setOrgs); - }, [retrieveOrgs]); - return ( + + Recover my Password + + + {error && ( + + + {error} + + + )} + {success && ( + + + {success} + + + )} + + Recover my password + + + ); +} + +export default RecoverForm; diff --git a/client/src/components/RegisterForm.jsx b/client/src/components/RegisterForm.jsx new file mode 100644 index 0000000..9aad97a --- /dev/null +++ b/client/src/components/RegisterForm.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Box, TextField, Typography } from '@mui/material'; + +import Form from '../Form'; +import { FormSubmit, FormTextField } from '../StyledElements'; + +function RegisterForm({ form, handleSubmit, handleChange, error }) { + return ( +
+ + + + + + + + {error && ( + + + {error} + + + )} + + Create an Account + + + Already have an account? Login + + + + ); +} + +export default RegisterForm; diff --git a/client/src/components/ResetForm.jsx b/client/src/components/ResetForm.jsx new file mode 100644 index 0000000..c26d419 --- /dev/null +++ b/client/src/components/ResetForm.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; + +import { FormSubmit, FormTextField } from '../StyledElements'; + +function ResetForm({ form, handleSubmit, handleChange, error }) { + return ( +
+ + Reset my Password + + + + {error && ( + + + {error} + + + )} + + Reset my Password + + + ); +} + +export default ResetForm; diff --git a/client/src/components/utils/StackItem.jsx b/client/src/components/StackItem.jsx similarity index 99% rename from client/src/components/utils/StackItem.jsx rename to client/src/components/StackItem.jsx index e4bb285..2bc4807 100644 --- a/client/src/components/utils/StackItem.jsx +++ b/client/src/components/StackItem.jsx @@ -1,5 +1,4 @@ import React from 'react'; - import { Paper, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; diff --git a/client/src/components/StyledElements.jsx b/client/src/components/StyledElements.jsx new file mode 100644 index 0000000..c8b11cb --- /dev/null +++ b/client/src/components/StyledElements.jsx @@ -0,0 +1,88 @@ +import { Button, FormGroup, Paper, Stack, TextField } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const Form = styled(FormGroup)(({ theme }) => ({ + [theme.breakpoints.only('xs')]: { + column: true, + }, +})); +const FormField = styled(TextField)(({ theme }) => ({ + [theme.breakpoints.only('xs')]: { + column: true, + width: '75%', + }, +})); +const FormButton = styled(Button)(({ theme }) => ({ + disableElevation: true, + color: theme.palette.primary.main, + [theme.breakpoints.only('xs')]: { + column: true, + width: '25%', + }, +})); + +const DynamicStack = styled(Stack)(({ theme }) => ({ + [theme.breakpoints.up('sm')]: { + spacing: 2, + alignItems: 'flex-end', + }, +})); + +const DynamicPaper = styled(Paper)(({ theme }) => ({ + ...theme.typography.body2, + padding: '2vh', + textAlign: 'center', + color: theme.palette.text.secondary, + flexGrow: 1, + margin: '0 2vh', + [theme.breakpoints.only('xs')]: { + height: '80vh', + }, + [theme.breakpoints.only('sm')]: { + height: '80vh', + }, + [theme.breakpoints.only('md')]: { + height: '82vh', + }, +})); + +const FormTextField = ({ label, name, value, onChange, required, other }) => { + return ( + + ); +}; + +const FormSubmit = ({ color, children, type, other }) => { + return ( + + ); +}; + +export { + Form, + FormField, + FormButton, + DynamicStack, + DynamicPaper, + FormSubmit, + FormTextField, +}; diff --git a/client/src/components/UpdateOrganizationForm.jsx b/client/src/components/UpdateOrganizationForm.jsx new file mode 100644 index 0000000..455e74f --- /dev/null +++ b/client/src/components/UpdateOrganizationForm.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Typography } from '@mui/material'; + +import Form from '../Form'; +import { FormSubmit, FormTextField } from '../StyledElements'; + +function UpdateOrganizationForm({ name, handleSubmit, handleChange }) { + return ( +
+ + + Rename Organization + + + + Rename + + + + ); +} + +export default UpdateOrganizationForm; diff --git a/client/src/components/forms/EditUser.jsx b/client/src/components/forms/EditUser.jsx deleted file mode 100644 index 936de71..0000000 --- a/client/src/components/forms/EditUser.jsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; - -import axios from 'axios'; -import Cookies from 'js-cookie'; - -import { Box, Typography, TextField, Button } from '@mui/material'; - -import Form from '../utils/Form'; -import RecordMp3 from '../utils/RecordMp3'; - -function EditUser() { - const navigate = useNavigate(); - - const [form, setForm] = useState({ - firstName: '', - lastName: '', - email: '', - pronouns: '', - }); - const [error, setError] = useState(''); - - const linkStyle = { textDecoration: 'none', color: 'inherit' }; - - useEffect(() => { - async function getInfo() { - axios - .get( - `${process.env.REACT_APP_API_ROOT}/auth/${Cookies.get( - 'userID' - )}?token=${Cookies.get('token')}` - ) - .then((response) => { - const user = response.data; - - setForm(() => { - return { - firstName: user.firstName, - lastName: user.lastName, - email: user.email, - pronouns: user.pronouns, - }; - }); - }); - } - getInfo(); - }, []); - - 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.REACT_APP_API_ROOT}/auth/update`, { - ...form, - token: Cookies.get('token'), - }) - .then(() => { - navigate('/'); - }) - .catch((e) => setError(() => e.response.data)); - } - - return ( -
- - - Update Account Details - - - - - - {error && ( - - - {error} - - - )} - - - - - - ); -} - -export default EditUser; diff --git a/client/src/components/forms/Login.jsx b/client/src/components/forms/Login.jsx deleted file mode 100644 index f5a2eb2..0000000 --- a/client/src/components/forms/Login.jsx +++ /dev/null @@ -1,154 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; - -import axios from 'axios'; -import Cookies from 'js-cookie'; - -import { - Box, - Typography, - TextField, - Button, - FormControlLabel, - Checkbox, -} from '@mui/material'; - -import Form from '../utils/Form'; - -function Login({ setToken }) { - const navigate = useNavigate(); - - const [form, setForm] = useState({ - email: '', - password: '', - remember: false, - }); - const [error, setError] = useState(''); - - useEffect(() => { - async function logout() { - await axios - .post(`${process.env.REACT_APP_API_ROOT}/auth/logout`, { - token: Cookies.get('token'), - }) - .catch((e) => { - console.log(e); - }); - - Cookies.remove('token'); - Cookies.remove('userID'); - - setToken(() => false); - } - - if (Cookies.get('token')) { - logout(); - } - }, [setToken]); - - function handleChange(e) { - const name = e.target.name; - const value = e.target.value; - - setForm((form) => { - return { ...form, [name]: value }; - }); - } - - function handleCheckboxChange(e) { - const name = e.target.name; - const checked = e.target.checked; - - setForm(() => { - return { ...form, [name]: checked }; - }); - } - - async function handleSubmit(e) { - e.preventDefault(); - await axios - .post(`${process.env.REACT_APP_API_ROOT}/auth/login`, form) - .then((response) => { - Cookies.set('token', response.data.token, { - expires: form.remember ? 3650 : 1, - }); - Cookies.set('userID', response.data.userID, { - expires: form.remember ? 3650 : 1, - }); - setToken(true); - navigate('/'); - }) - .catch((e) => setError(() => e.response.data)); - } - - return ( -
- - - Login - - - -
- } - labelPlacement='start' - label='Remember me' - name='remember' - onChange={handleCheckboxChange} - /> -
- {error && ( - - - {error} - - - )} - - - Dont have an account? Register - - - Forgot your password?{' '} - Recover Password - - - - ); -} - -export default Login; diff --git a/client/src/components/forms/Recover.jsx b/client/src/components/forms/Recover.jsx deleted file mode 100644 index 70ac74f..0000000 --- a/client/src/components/forms/Recover.jsx +++ /dev/null @@ -1,76 +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.REACT_APP_API_ROOT}/auth/reset-password?${form.email}` - ) - .then((response) => { - setSuccess(() => response.data); - }) - .catch((e) => setError(() => e.response.data)); - } - - return ( -
- - Recover my Password - - - {error && ( - - - {error} - - - )} - {success && ( - - - {success} - - - )} - - - ); -} - -export default Recover; diff --git a/client/src/components/forms/Register.jsx b/client/src/components/forms/Register.jsx deleted file mode 100644 index 721600b..0000000 --- a/client/src/components/forms/Register.jsx +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState } from 'react'; -import { Link, useNavigate, useSearchParams } from 'react-router-dom'; - -import axios from 'axios'; - -import { Box, Typography, TextField, Button } from '@mui/material'; - -import Form from '../utils/Form'; - -function Register() { - const navigate = useNavigate(); - const [params] = useSearchParams(); - - const [form, setForm] = useState({ - firstName: '', - lastName: '', - email: '', - pronouns: '', - 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(); - - const { confirmPassword, ...filteredForm } = form; - await axios - .post(`${process.env.REACT_APP_API_ROOT}/auth/register`, { - ...filteredForm, - orgID: params.orgID, - }) - .then(() => { - navigate('/login'); - }) - .catch((e) => setError(() => e.response.data)); - } - - return ( -
- - - Create an Account - - - - - - - - {error && ( - - - {error} - - - )} - - - Already have an account? Login - - - - ); -} - -export default Register; diff --git a/client/src/components/forms/Reset.jsx b/client/src/components/forms/Reset.jsx deleted file mode 100644 index f605f1f..0000000 --- a/client/src/components/forms/Reset.jsx +++ /dev/null @@ -1,90 +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.REACT_APP_API_ROOT}/auth/reset-password/${id}`, - { - password: form.password, - } - ) - .then(() => { - navigate('/login'); - }) - .catch((e) => setError(() => e.response.data)); - } - - return ( -
- - Reset my Password - - - - {error && ( - - - {error} - - - )} - - - ); -} - -export default Reset; diff --git a/client/src/components/forms/UpdateOrganization.jsx b/client/src/components/forms/UpdateOrganization.jsx deleted file mode 100644 index d9af585..0000000 --- a/client/src/components/forms/UpdateOrganization.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; - -import axios from 'axios'; -import Cookies from 'js-cookie'; - -import { Typography, TextField, Button } from '@mui/material'; - -import Form from '../utils/Form'; - -function UpdateOrganization() { - const { orgID } = useParams(); - const navigate = useNavigate(); - - const [name, setName] = useState(''); - - function handleChange(e) { - const value = e.target.value; - - setName(() => value); - } - - async function handleSubmit(e) { - e.preventDefault(); - await axios - .patch(`${process.env.REACT_APP_API_ROOT}/org/update`, { - token: Cookies.get('token'), - orgName: name, - orgID, - }) - .then(() => { - navigate(`/org/${orgID}`); - }); - } - - return ( -
- - - Rename Organization - - - - - - ); -} - -export default UpdateOrganization; diff --git a/client/src/components/orgs/OrgDashboard.jsx b/client/src/components/orgs/OrgDashboard.jsx deleted file mode 100644 index 226b51c..0000000 --- a/client/src/components/orgs/OrgDashboard.jsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useNavigate, useParams, Link } from 'react-router-dom'; - -import axios from 'axios'; -import Cookies from 'js-cookie'; - -import { - Typography, - Grid, - Stack, - Box, - ButtonGroup, - Button, - Accordion, - AccordionSummary, - AccordionDetails, - useMediaQuery, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; - -import Dashboard from '../utils/Dashboard'; - -const DynamicStack = styled(Stack)(({ theme }) => ({ - [theme.breakpoints.up('sm')]: { - spacing: 2, - alignItems: 'flex-end', - }, -})); - -function OrgDashboard() { - const xs = useMediaQuery((theme) => theme.breakpoints.only('xs')); - const { orgID } = useParams(); - const navigate = useNavigate(); - - const [org, setOrg] = useState({ - id: orgID, - name: '', - owner: { - firstName: '', - lastName: '', - email: '', - }, - member: [], - createdAt: '', - memberCount: 0, - }); - const [rows, setRows] = useState([]); - const [status, setStatus] = useState(true); - const [selection, setSelection] = useState(null); - const [open, setOpen] = useState(false); - - const linkStyle = { textDecoration: 'none', color: 'inherit' }; - const buttonGroupOrientation = () => { - if (xs) return 'vertical'; - }; - - useEffect(() => { - async function getData() { - await axios - .get( - `${ - process.env.REACT_APP_API_ROOT - }/org/${orgID}?token=${Cookies.get('token')}` - ) - .then((response) => { - const res = response.data; - - setOrg(() => { - return { ...res.org, memberCount: res.memberCount }; - }); - - setRows(() => res.org.member); - setStatus(() => res.status); - }) - .catch((e) => { - console.log(e); - navigate('/'); - }); - } - - getData(); - }, [navigate, orgID]); - - async function handleDelete() { - await axios - .delete(`${process.env.REACT_APP_API_ROOT}/org/delete`, { - params: { - token: Cookies.get('token'), - orgID, - }, - }) - .then(navigate('/')); - } - - function copyID() { - navigator.clipboard.writeText(orgID); - } - - function copyJoinLink() { - navigator.clipboard.writeText( - `${process.env.REACT_APP_DOMAIN_ROOT}/register?orgID=${orgID}` - ); - } - - async function removeSelected() { - const doomedUserIDs = selection.map((row) => row.id); - - await axios.post( - `${process.env.REACT_APP_API_ROOT}/org/${orgID}/delete`, - { - token: Cookies.get('token'), - orgID, - doomedUserIDs, - } - ); - } - - async function leaveOrg() { - await axios - .post(`${process.env.REACT_APP_API_ROOT}/org/${orgID}/delete`, { - token: Cookies.get('token'), - orgID, - }) - .then(() => navigate('/')) - .catch((e) => { - console.log(e); - navigate('/'); - }); - } - - function handleOpen() { - setOpen((initial) => (initial ? false : true)); - } - - async function play(id) { - axios - .get( - `${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${id}.mp3`, - { - responseType: 'blob', - } - ) - .then((res) => { - const uploadedFile = new File([res.data], 'userAudio.mp3', { - type: res.data.type, - lastModified: Date.now(), - }); - const audioFile = new Audio(URL.createObjectURL(uploadedFile)); - audioFile.play(); - }); - } - - return ( - - - }> - - Organizations/{org.name} - - - - - - - - - Organization ID: {orgID} - - - Members: {org.memberCount} - - - Created On: {org.createdAt} - - - - - - - Owner:{' '} - {`${org.owner.firstName} ${org.owner.lastName}`} - - - Email: {org.owner.email} - - - - - - {status ? ( - - - - - - - - - - - - ) : ( - - )} - - - - - - - - ); -} - -export default OrgDashboard; diff --git a/client/src/components/utils/Dashboard.jsx b/client/src/components/utils/Dashboard.jsx deleted file mode 100644 index 5ebb5c8..0000000 --- a/client/src/components/utils/Dashboard.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; - -import { Paper, Box, IconButton, useMediaQuery, Collapse } from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { DataGrid } from '@mui/x-data-grid'; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; - -import MemberUnit from '../utils/MemberUnit'; - -const DynamicPaper = styled(Paper)(({ theme }) => ({ - ...theme.typography.body2, - padding: '2vh', - textAlign: 'center', - color: theme.palette.text.secondary, - flexGrow: 1, - margin: '0 2vh', - [theme.breakpoints.only('xs')]: { - height: '80vh', - }, - [theme.breakpoints.only('sm')]: { - height: '80vh', - }, - [theme.breakpoints.only('md')]: { - height: '82vh', - }, -})); - -function Dashboard({ children, rows, setSelection, onClick, open }) { - const xs = useMediaQuery((theme) => theme.breakpoints.only('xs')); - const sm = useMediaQuery((theme) => theme.breakpoints.only('sm')); - const md = useMediaQuery((theme) => theme.breakpoints.only('md')); - - const columns = [ - { field: 'id', headerName: 'ID', flex: 1 }, - { field: 'firstName', headerName: 'First Name', flex: 3 }, - { field: 'lastName', headerName: 'Last Name', flex: 3 }, - { field: 'nickname', headerName: 'Nickname', flex: 2 }, - { - field: 'namePronounciation', - headerName: 'Pronounciation', - flex: 2, - sortable: false, - renderCell: (params) => ( - onClick(params.id)} - > - - - ), - }, - { field: 'pronouns', headerName: 'Pronouns', flex: 2 }, - { field: 'email', headerName: 'Email', flex: 10, sortable: false }, - ]; - - function calculateCollapsedSize() { - if (xs) return '37vh'; - if (sm) return '63vh'; - if (md) return '68vh'; - } - - function calculateTableHeight() { - return open ? '64vh' : '100%'; - } - - return ( - - {children} - - - {(xs || sm) && - rows.map((member) => { - return ( - - ); - })} - {md && ( - - { - setSelection(() => newSelection.rows); - }} - checkboxSelection - /> - - )} - - - - ); -} - -export default Dashboard; diff --git a/client/src/components/utils/Form.jsx b/client/src/components/utils/Form.jsx deleted file mode 100644 index a4c6235..0000000 --- a/client/src/components/utils/Form.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { Outlet } from 'react-router-dom'; - -import { Box, Grid, Paper } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; - -function Form({ children }) { - const theme = useTheme(); - - return ( - - - - - - - {children} - - - - - - - - ); -} - -export default Form; diff --git a/client/src/controllers/EditUser.jsx b/client/src/controllers/EditUser.jsx new file mode 100644 index 0000000..cbeca99 --- /dev/null +++ b/client/src/controllers/EditUser.jsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import Cookies from 'js-cookie'; + +import EditUserForm from '../components/EditUserForm'; + +function EditUser() { + const navigate = useNavigate(); + + const [form, setForm] = useState({ + firstName: '', + lastName: '', + email: '', + pronouns: '', + }); + const [error, setError] = useState(''); + + useEffect(() => { + async function getInfo() { + axios + .get( + `${process.env.REACT_APP_API_ROOT}/auth/${Cookies.get( + 'userID' + )}?token=${Cookies.get('token')}` + ) + .then((response) => { + const user = response.data; + + setForm(() => { + return { + firstName: user.firstName, + lastName: user.lastName, + email: user.email, + pronouns: user.pronouns, + }; + }); + }); + } + getInfo(); + }, []); + + 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.REACT_APP_API_ROOT}/auth/update`, { + ...form, + token: Cookies.get('token'), + }) + .then(() => { + navigate('/'); + }) + .catch((e) => setError(() => e.response.data)); + } + + return ( + + ); +} + +export default EditUser; diff --git a/client/src/controllers/Login.jsx b/client/src/controllers/Login.jsx new file mode 100644 index 0000000..1399ac1 --- /dev/null +++ b/client/src/controllers/Login.jsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import Cookies from 'js-cookie'; + +import LoginForm from '../components/LoginForm'; + +function Login({ setToken }) { + const navigate = useNavigate(); + + const [form, setForm] = useState({ + email: '', + password: '', + remember: false, + }); + const [error, setError] = useState(''); + + useEffect(() => { + async function logout() { + await axios + .post(`${process.env.REACT_APP_API_ROOT}/auth/logout`, { + token: Cookies.get('token'), + }) + .catch((e) => { + console.log(e); + }); + + Cookies.remove('token'); + Cookies.remove('userID'); + + setToken(() => false); + } + + if (Cookies.get('token')) { + logout(); + } + }, [setToken]); + + function handleChange(e) { + const name = e.target.name; + const value = e.target.value; + + setForm((form) => { + return { ...form, [name]: value }; + }); + } + + function handleCheckboxChange(e) { + const name = e.target.name; + const checked = e.target.checked; + + setForm(() => { + return { ...form, [name]: checked }; + }); + } + + async function handleSubmit(e) { + e.preventDefault(); + await axios + .post(`${process.env.REACT_APP_API_ROOT}/auth/login`, form) + .then((response) => { + Cookies.set('token', response.data.token, { + expires: form.remember ? 3650 : 1, + }); + Cookies.set('userID', response.data.userID, { + expires: form.remember ? 3650 : 1, + }); + setToken(true); + navigate('/'); + }) + .catch((e) => setError(() => e.response.data)); + } + + return ( + + ); +} + +export default Login; diff --git a/client/src/controllers/MainPage.jsx b/client/src/controllers/MainPage.jsx new file mode 100644 index 0000000..4aff989 --- /dev/null +++ b/client/src/controllers/MainPage.jsx @@ -0,0 +1,101 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; +import Cookies from 'js-cookie'; + +import MainPageBase from '../components/MainPageBase'; + +function MainPage() { + const navigate = useNavigate(); + + const [ownedOrgs, setOwnedOrgs] = useState([]); + const [orgs, setOrgs] = useState([]); + const [form, setForm] = useState({ + name: '', + id: '', + }); + + useEffect(() => { + if (Cookies.get('token')) { + getMemberOrgs(); + getOwnedOrgs(); + } else { + navigate('/login'); + } + }, [navigate]); + + function handleChange(e) { + const name = e.target.name; + const value = e.target.value; + + setForm((form) => { + return { ...form, [name]: value }; + }); + } + + async function getOwnedOrgs() { + await axios + .get( + `${process.env.REACT_APP_API_ROOT}/auth?token=${Cookies.get( + 'token' + )}` + ) + .then((response) => { + setOwnedOrgs(() => response.data); + }) + .catch((e) => { + console.log(e); + }); + } + + async function getMemberOrgs() { + await axios + .get( + `${ + process.env.REACT_APP_API_ROOT + }/auth/orgs?token=${Cookies.get('token')}` + ) + .then((response) => { + setOrgs(() => response.data); + }); + } + + async function createOrg(e) { + e.preventDefault(); + console.log('create'); + + await axios + .post(`${process.env.REACT_APP_API_ROOT}/org/create`, { + token: Cookies.get('token'), + orgName: form.name, + }) + .then(() => { + getOwnedOrgs(); + }); + } + + async function joinOrg(e) { + e.preventDefault(); + + await axios + .post(`${process.env.REACT_APP_API_ROOT}/org/${form.id}/add`, { + token: Cookies.get('token'), + }) + .then(() => { + getMemberOrgs(); + }); + } + + return ( + + ); +} + +export default MainPage; diff --git a/client/src/components/orgs/MemberOrgs.jsx b/client/src/controllers/MemberOrgs.jsx similarity index 61% rename from client/src/components/orgs/MemberOrgs.jsx rename to client/src/controllers/MemberOrgs.jsx index cfd10fb..17a88e6 100644 --- a/client/src/components/orgs/MemberOrgs.jsx +++ b/client/src/controllers/MemberOrgs.jsx @@ -1,12 +1,13 @@ -import React from 'react'; - +import React, { useEffect, useState } from 'react'; import axios from 'axios'; import Cookies from 'js-cookie'; -import Organizations from '../utils/Organizations'; +import Organizations from '../components/Organizations'; function JoinedOrgs() { - async function getOrgs(setOrgs) { + const [orgs, setOrgs] = useState([]); + + async function getOrgs() { axios .get( `${ @@ -18,7 +19,9 @@ function JoinedOrgs() { }); } - return ; + useEffect(getOrgs); + + return ; } export default JoinedOrgs; diff --git a/client/src/controllers/OrgDashboard.jsx b/client/src/controllers/OrgDashboard.jsx new file mode 100644 index 0000000..ca29fff --- /dev/null +++ b/client/src/controllers/OrgDashboard.jsx @@ -0,0 +1,136 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from 'axios'; +import Cookies from 'js-cookie'; + +import Dashboard from '../components/Dashboard'; + +function OrgDashboard() { + const { orgID } = useParams(); + const navigate = useNavigate(); + + const [org, setOrg] = useState({ + id: orgID, + name: '', + owner: { + firstName: '', + lastName: '', + email: '', + }, + member: [], + createdAt: '', + memberCount: 0, + }); + const [rows, setRows] = useState([]); + const [status, setStatus] = useState(true); + const [selection, setSelection] = useState(null); + + useEffect(() => { + async function getData() { + await axios + .get( + `${ + process.env.REACT_APP_API_ROOT + }/org/${orgID}?token=${Cookies.get('token')}` + ) + .then((response) => { + const res = response.data; + + setOrg(() => { + return { ...res.org, memberCount: res.memberCount }; + }); + + setRows(() => res.org.member); + setStatus(() => res.status); + }) + .catch((e) => { + console.log(e); + navigate('/'); + }); + } + + getData(); + }, [navigate, orgID]); + + async function handleDelete() { + await axios + .delete(`${process.env.REACT_APP_API_ROOT}/org/delete`, { + params: { + token: Cookies.get('token'), + orgID, + }, + }) + .then(navigate('/')); + } + + function copyID() { + navigator.clipboard.writeText(orgID); + } + + function copyJoinLink() { + navigator.clipboard.writeText( + `${process.env.REACT_APP_DOMAIN_ROOT}/register?orgID=${orgID}` + ); + } + + async function removeSelected() { + const doomedUserIDs = selection.map((row) => row.id); + + await axios.post( + `${process.env.REACT_APP_API_ROOT}/org/${orgID}/delete`, + { + token: Cookies.get('token'), + orgID, + doomedUserIDs, + } + ); + } + + async function leaveOrg() { + await axios + .post(`${process.env.REACT_APP_API_ROOT}/org/${orgID}/delete`, { + token: Cookies.get('token'), + orgID, + }) + .then(() => navigate('/')) + .catch((e) => { + console.log(e); + navigate('/'); + }); + } + + async function play(id) { + await axios + .get( + `${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${id}.mp3`, + { + responseType: 'blob', + } + ) + .then((res) => { + const uploadedFile = new File([res.data], 'userAudio.mp3', { + type: res.data.type, + lastModified: Date.now(), + }); + const audioFile = new Audio(URL.createObjectURL(uploadedFile)); + audioFile.play(); + }); + } + + return ( + + ); +} + +export default OrgDashboard; diff --git a/client/src/components/orgs/OwnedOrgs.jsx b/client/src/controllers/OwnedOrgs.jsx similarity index 61% rename from client/src/components/orgs/OwnedOrgs.jsx rename to client/src/controllers/OwnedOrgs.jsx index a19607a..a1e3a42 100644 --- a/client/src/components/orgs/OwnedOrgs.jsx +++ b/client/src/controllers/OwnedOrgs.jsx @@ -1,12 +1,13 @@ -import React from 'react'; - +import React, { useEffect, useState } from 'react'; import axios from 'axios'; import Cookies from 'js-cookie'; -import Organizations from '../utils/Organizations'; +import Organizations from '../components/Organizations'; function JoinedOrgs() { - async function getOrgs(setOrgs) { + const [orgs, setOrgs] = useState([]); + + async function getOrgs() { axios .get( `${process.env.REACT_APP_API_ROOT}/auth?token=${Cookies.get( @@ -18,7 +19,9 @@ function JoinedOrgs() { }); } - return ; + useEffect(getOrgs); + + return ; } export default JoinedOrgs; diff --git a/client/src/controllers/Recover.jsx b/client/src/controllers/Recover.jsx new file mode 100644 index 0000000..01a3d23 --- /dev/null +++ b/client/src/controllers/Recover.jsx @@ -0,0 +1,45 @@ +import React, { useState } from 'react'; +import axios from 'axios'; + +import RecoverForm from '../components/RecoverForm'; + +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.REACT_APP_API_ROOT}/auth/reset-password?${form.email}` + ) + .then((response) => { + setSuccess(() => response.data); + }) + .catch((e) => setError(() => e.response.data)); + } + + return ( + + ); +} + +export default Recover; diff --git a/client/src/controllers/Register.jsx b/client/src/controllers/Register.jsx new file mode 100644 index 0000000..f946069 --- /dev/null +++ b/client/src/controllers/Register.jsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import axios from 'axios'; + +import RegisterForm from '../components/RegisterForm'; + +function Register() { + const navigate = useNavigate(); + const [params] = useSearchParams(); + + const [form, setForm] = useState({ + firstName: '', + lastName: '', + email: '', + pronouns: '', + 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(); + + const { confirmPassword, ...filteredForm } = form; + await axios + .post(`${process.env.REACT_APP_API_ROOT}/auth/register`, { + ...filteredForm, + orgID: params.orgID, + }) + .then(() => { + navigate('/login'); + }) + .catch((e) => setError(() => e.response.data)); + } + + return ( + + ); +} + +export default Register; diff --git a/client/src/controllers/Reset.jsx b/client/src/controllers/Reset.jsx new file mode 100644 index 0000000..98631bf --- /dev/null +++ b/client/src/controllers/Reset.jsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from 'axios'; + +import ResetForm from '../components/ResetForm'; + +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.REACT_APP_API_ROOT}/auth/reset-password/${id}`, + { + password: form.password, + } + ) + .then(() => { + navigate('/login'); + }) + .catch((e) => setError(() => e.response.data)); + } + + return ( + + ); +} + +export default Reset; diff --git a/client/src/controllers/UpdateOrganization.jsx b/client/src/controllers/UpdateOrganization.jsx new file mode 100644 index 0000000..219d3b6 --- /dev/null +++ b/client/src/controllers/UpdateOrganization.jsx @@ -0,0 +1,42 @@ +import React, { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import axios from 'axios'; +import Cookies from 'js-cookie'; + +import UpdateOrganizationForm from '../components/UpdateOrganizationForm'; + +function UpdateOrganization() { + const { orgID } = useParams(); + const navigate = useNavigate(); + + const [name, setName] = useState(''); + + function handleChange(e) { + const value = e.target.value; + + setName(() => value); + } + + async function handleSubmit(e) { + e.preventDefault(); + await axios + .patch(`${process.env.REACT_APP_API_ROOT}/org/update`, { + token: Cookies.get('token'), + orgName: name, + orgID, + }) + .then(() => { + navigate(`/org/${orgID}`); + }); + } + + return ( + + ); +} + +export default UpdateOrganization;