Compare commits

..

10 commits

170 changed files with 11399 additions and 33038 deletions

View file

@ -1 +0,0 @@
node_modules

12
.eslintrc.yml Normal file
View file

@ -0,0 +1,12 @@
env:
browser: true
es2021: true
extends: [airbnb, "prettier"]
parserOptions:
ecmaVersion: latest
sourceType: module
project: ./tsconfig.json
ignorePatterns: ["vite.config.js"]
rules:
indent: ["error", 4]
comma-dangle: "off"

View file

@ -1,31 +0,0 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: 'Features'
labels:
- 'feature'
- 'enhancement'
- title: 'Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: 'Maintenance'
label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&#@' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
# Changes
$CHANGES

View file

@ -1,38 +0,0 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Build release assets
on:
release:
types: [published]
jobs:
draft:
environment: production
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Write .env to build
env:
DOTENV: '${{ secrets.DOTENV }}'
run: echo "$DOTENV" >> .env
- name: Create public assets
run: mkdir ./server/public ./server/public/audio ./server/public/images
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Install dependencies and build
run: npm run dist
- name: Upload release assets
id: upload-release-asset
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: ./dist.tar.gz
asset_name: $tag-dist.tar.gz
asset_content_type: application/gzip
overwrite: true

View file

@ -1,21 +0,0 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Create release draft
on:
push:
branches: [dev]
workflow_dispatch:
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Draft release
uses: release-drafter/release-drafter@v5
id: release-draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,30 +0,0 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Test build
on:
push:
branches: [dev, main]
pull_request:
workflow_dispatch:
jobs:
test:
environment: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Write ./server/.env to build
env:
DOTENV: '${{ secrets.DOTENV }}'
run: echo "$DOTENV" >> ./server/.env
- name: Use Node.js 16.x
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Install dependencies
run: npm ci
- name: Test build
run: npm run test

25
client/.eslintrc.yml Normal file
View file

@ -0,0 +1,25 @@
root: false
env: { browser: true, es2020: true }
extends:
- "eslint:recommended"
- "airbnb-typescript"
- "plugin:@typescript-eslint/recommended"
- "plugin:react-hooks/recommended"
ignorePatterns: ["dist", ".eslintrc.cjs"]
parser: "@typescript-eslint/parser"
plugins: ["react-refresh"]
rules:
"react-refresh/only-export-components":
["warn", { allowConstantExport: true }]
"react/jsx-indent": ["error", 4]
"react/jsx-indent-props": ["error", 4]
"react/jsx-props-no-spreading": "off"
"react/react-in-jsx-scope": "off"
"react/require-default-props": "off"
"@typescript-eslint/indent": ["error", 4]
"@typescript-eslint/quotes": ["error", "double"]
"@typescript-eslint/no-shadow": "off"
"@typescript-eslint/comma-dangle": "off"
"import/no-extraneous-dependencies": "off"
"import/no-cycle": "off"
"jsx-a11y/label-has-associated-control": "off"

41
client/.gitignore vendored
View file

@ -1,23 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View file

@ -1,9 +1,22 @@
# syntax=docker/dockerfile:1
FROM node:16-alpine
FROM node:16-alpine AS build
WORKDIR /app
COPY ["package.json", "./"]
RUN apk update
RUN npm install
COPY . .
RUN npm run build
FROM node:16-alpine AS production
WORKDIR /app
COPY --from=build /app/build ./build
RUN npm install -g serve
EXPOSE 3000/tcp
CMD npm start
CMD ["npx", "serve", "-s", "build"]

14
client/Dockerfile.dev Normal file
View file

@ -0,0 +1,14 @@
# syntax=docker/dockerfile:1
FROM node:16-alpine
WORKDIR /app
COPY ["package.json", "./"]
RUN npm install
COPY . .
EXPOSE 3000/tcp
CMD ["npm", "run", "start"]

13
client/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View file

@ -1,49 +1,47 @@
{
"name": "client",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.8.1",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.4.4",
"@mui/material": "^5.4.4",
"@mui/x-data-grid": "^5.6.0",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.26.1",
"buffer": "^6.0.3",
"js-cookie": "^3.0.1",
"mic-recorder-to-mp3": "^2.2.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
"name": "client",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.10",
"@mui/material": "^5.15.10",
"@mui/x-data-grid": "^6.19.4",
"axios": "^1.6.7",
"buffer": "^6.0.3",
"js-cookie": "^3.0.5",
"mic-recorder-to-mp3": "^2.2.2",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^6.22.1",
"recordrtc": "^5.6.2"
},
"devDependencies": {
"@mui/types": "^7.2.13",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.19",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-helmet": "^6.1.11",
"@types/recordrtc": "^5.6.14",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.1.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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>KnowItAll</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
--></body>
</html>

View file

@ -1,15 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -1,52 +0,0 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import {
Layout,
Login,
Register,
MainPage,
EditUser,
Recover,
Reset,
OwnedOrgs,
MemberOrgs,
OrgDashboard,
} from './containers';
import { ContextProvider } from './contexts';
import UpdateOrganization from './controllers/UpdateOrganizationFormController';
function App() {
return (
<BrowserRouter>
<ContextProvider>
<Routes>
<Route path='' element={<Layout />}>
<Route index element={<MainPage />} />
<Route path='/login' element={<Login />} />
<Route path='/register' element={<Register />} />
<Route path='/update' element={<EditUser />} />
<Route path='recover'>
<Route index element={<Recover />} />
<Route path=':id' element={<Reset />} />
</Route>
<Route path='org'>
<Route index element={<OwnedOrgs />} />
<Route path='joined' element={<MemberOrgs />} />
<Route path=':orgID'>
<Route index element={<OrgDashboard />} />
<Route path='update' element={<UpdateOrganization />} />
</Route>
</Route>
<Route path='*' element={<Navigate to='' />} />
</Route>
</Routes>
</ContextProvider>
</BrowserRouter>
);
}
export default App;

56
client/src/App.tsx Normal file
View file

@ -0,0 +1,56 @@
import {
BrowserRouter, Routes, Route, Navigate,
} from "react-router-dom";
import {
Layout,
MainPage,
Login,
Register,
EditUser,
Recover,
Reset,
OwnedOrgs,
MemberOrgs,
OrgDashboard,
UpdateOrganization,
} from "./containers";
import { ContextProvider } from "./contexts";
function App() {
return (
<BrowserRouter>
<ContextProvider>
<Routes>
<Route path="" element={<Layout />}>
<Route index element={<MainPage />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/update" element={<EditUser />} />
<Route path="recover">
<Route index element={<Recover />} />
<Route path=":id" element={<Reset />} />
</Route>
<Route path="org">
<Route index element={<OwnedOrgs />} />
<Route path="joined" element={<MemberOrgs />} />
<Route path=":orgID">
<Route index element={<OrgDashboard />} />
<Route
path="update"
element={<UpdateOrganization />}
/>
</Route>
</Route>
<Route path="*" element={<Navigate to="" />} />
</Route>
</Routes>
</ContextProvider>
</BrowserRouter>
);
}
export default App;

View file

@ -1,4 +1,5 @@
import { Link } from 'react-router-dom';
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import {
Accordion,
AccordionDetails,
@ -12,15 +13,31 @@ import {
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';
Theme,
} from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
import { Link } from "react-router-dom";
import { MemberUnit } from '.';
import { DynamicPaper, DynamicStack } from './StyledElements';
import MemberUnit from "./MemberUnit";
import { DynamicPaper, DynamicStack } from "./StyledElements";
import { Organization, User } from "../types";
function Dashboard({
interface DashboardProps {
rows: User[];
open: boolean;
handleOpen: () => void;
setSelection: (newSelection: User[]) => void;
onClick: (id: number) => void;
org: Organization;
status: boolean;
handleDelete: () => void;
copyID: () => void;
copyJoinLink: () => void;
removeSelected: () => void;
leaveOrg: () => void;
}
export default function Dashboard({
rows,
open,
handleOpen,
@ -33,28 +50,28 @@ function Dashboard({
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'));
}: DashboardProps) {
const xs = useMediaQuery((theme: Theme) => theme.breakpoints.only("xs"));
const sm = useMediaQuery((theme: Theme) => theme.breakpoints.only("sm"));
const md = useMediaQuery((theme: Theme) => theme.breakpoints.only("md"));
const linkStyle = { textDecoration: 'none', color: 'inherit' };
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: "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',
field: "namePronounciation",
headerName: "Pronounciation",
flex: 2,
sortable: false,
renderCell: (params) => (
renderCell: (params: User) => (
<IconButton
size='large'
edge='start'
color='inherit'
aria-label='menu'
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
onClick={() => onClick(params.id)}
>
@ -62,53 +79,63 @@ function Dashboard({
</IconButton>
),
},
{ field: 'pronouns', headerName: 'Pronouns', flex: 2 },
{ field: 'email', headerName: 'Email', flex: 10, sortable: false },
{ field: "pronouns", headerName: "Pronouns", flex: 2 },
{
field: "email",
headerName: "Email",
flex: 10,
sortable: false,
},
];
const buttonGroupOrientation = () => {
if (xs) return 'vertical';
};
const buttonGroupOrientation = () => (xs ? "vertical" : "horizontal");
function calculateCollapsedSize() {
if (xs) return '37vh';
if (sm) return '63vh';
if (md) return '68vh';
if (xs) return "37vh";
if (sm) return "63vh";
return "68vh";
}
function calculateTableHeight() {
return open ? '64vh' : '100%';
return open ? "64vh" : "100%";
}
return (
<>
<Box sx={{ padding: '2vh' }}>
<Box sx={{ padding: "2vh" }}>
<Accordion expanded={open} onChange={handleOpen}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant='h6'>Organizations / {org.name}</Typography>
<Typography variant="h6">
Organizations / {org.name}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ marginTop: '1vh' }} fullWidth>
<Grid container columns={{ xs: 3, sm: 12, md: 12 }} spacing={1}>
<Box>
<Grid
container
columns={{ xs: 3, sm: 12, md: 12 }}
spacing={1}
>
<Grid item xs={3}>
<Stack>
<Typography variant='body1'>
<Typography variant="body1">
Organization ID: {org.id}
</Typography>
<Typography variant='body1'>
<Typography variant="body1">
Members: {org.memberCount}
</Typography>
<Typography variant='body1'>
<Typography variant="body1">
Created On: {org.createdAt}
</Typography>
</Stack>
</Grid>
<Grid item xs={3}>
<Stack>
<Typography variant='body1'>
Owner: {`${org.owner.firstName} ${org.owner.lastName}`}
<Typography variant="body1">
Owner:{" "}
{`${org.owner.firstName} ${org.owner.lastName}`}
</Typography>
<Typography variant='body1'>
<Typography variant="body1">
Email: {org.owner.email}
</Typography>
</Stack>
@ -118,35 +145,53 @@ function Dashboard({
{status ? (
<>
<ButtonGroup
variant='outlined'
variant="outlined"
orientation={buttonGroupOrientation()}
>
<Button color='warning'>
<Link to='update' style={linkStyle}>
<Button color="warning">
<Link
to="update"
style={linkStyle}
>
Edit {org.name}
</Link>
</Button>
<Button color='error' onClick={handleDelete}>
<Button
color="error"
onClick={handleDelete}
>
Delete {org.name}
</Button>
</ButtonGroup>
<ButtonGroup
variant='outlined'
variant="outlined"
orientation={buttonGroupOrientation()}
>
<Button color='success' onClick={copyID}>
<Button
color="success"
onClick={copyID}
>
Copy Org ID
</Button>
<Button color='success' onClick={copyJoinLink}>
<Button
color="success"
onClick={copyJoinLink}
>
Copy Org Join Link
</Button>
<Button color='error' onClick={removeSelected}>
<Button
color="error"
onClick={removeSelected}
>
Remove People
</Button>
</ButtonGroup>
</>
) : (
<Button color='error' onClick={leaveOrg}>
<Button
color="error"
onClick={leaveOrg}
>
Leave Org
</Button>
)}
@ -157,12 +202,12 @@ function Dashboard({
</AccordionDetails>
</Accordion>
</Box>
<Collapse collapsedSize={calculateCollapsedSize()} in={open ? false : true}>
<Collapse collapsedSize={calculateCollapsedSize()} in={!open}>
<DynamicPaper>
{(xs || sm) &&
rows.map((member) => {
return <MemberUnit member={member} onClick={onClick} />;
})}
rows.map((member) => (
<MemberUnit member={member} onClick={onClick} />
))}
{md && (
<Box height={calculateTableHeight()}>
<DataGrid
@ -182,5 +227,3 @@ function Dashboard({
</>
);
}
export default Dashboard;

View file

@ -1,64 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Box, Typography } from '@mui/material';
import { Form } from '.';
import { RecordMp3 } from '../controllers';
import { FormSubmit, FormTextField } from './StyledElements';
function EditUserForm({ form, error, handleSubmit, handleChange }) {
const linkStyle = { textDecoration: 'none', color: 'inherit' };
return (
<Form text='Update Account Details'>
<form onSubmit={handleSubmit}>
<FormTextField
required
label='First Name'
name='firstName'
onChange={handleChange}
value={form.firstName}
/>
<FormTextField
required
label='Last Name'
name='lastName'
onChange={handleChange}
value={form.lastName}
/>
<FormTextField
required
label='Email'
name='email'
onChange={handleChange}
value={form.email}
/>
<FormTextField
required
label='Pronouns'
name='pronouns'
onChange={handleChange}
value={form.pronouns}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='secondary'>
{error}
</Typography>
</Box>
)}
<FormSubmit color='primary' type='submit'>
Update Details
</FormSubmit>
<FormSubmit color='error' type='button'>
<Link to='/recover' style={linkStyle}>
Reset Your Password
</Link>
</FormSubmit>
</form>
<RecordMp3 />
</Form>
);
}
export default EditUserForm;

View file

@ -0,0 +1,80 @@
import { Box, Typography } from "@mui/material";
import { Link } from "react-router-dom";
import Form from "./Form";
import { RecordMp3 } from "../controllers";
import { FormSubmit, FormTextField } from "./StyledElements";
interface EditUserFormProps {
form: {
firstName: string;
lastName: string;
email: string;
pronouns: string;
};
error: string;
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
function EditUserForm({
form,
error,
handleSubmit,
handleChange,
}: EditUserFormProps) {
const linkStyle = { textDecoration: "none", color: "inherit" };
return (
<Form text="Update Account Details">
<form onSubmit={handleSubmit}>
<FormTextField
required
label="First Name"
name="firstName"
onChange={handleChange}
value={form.firstName}
/>
<FormTextField
required
label="Last Name"
name="lastName"
onChange={handleChange}
value={form.lastName}
/>
<FormTextField
required
label="Email"
name="email"
onChange={handleChange}
value={form.email}
/>
<FormTextField
required
label="Pronouns"
name="pronouns"
onChange={handleChange}
value={form.pronouns}
/>
{error && (
<Box textAlign="right">
<Typography variant="body2" color="secondary">
{error}
</Typography>
</Box>
)}
<FormSubmit color="primary" type="submit">
Update Details
</FormSubmit>
<FormSubmit color="error" type="button">
<Link to="/recover" style={linkStyle}>
Reset Your Password
</Link>
</FormSubmit>
</form>
<RecordMp3 />
</Form>
);
}
export default EditUserForm;

View file

@ -1,36 +1,51 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Box, Grid, Card, Typography, CardContent, Alert } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
Alert, Box, Card, CardContent, Grid, Typography,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { Outlet } from "react-router-dom";
function Form({ children, text, error }) {
interface FormProps {
children: React.ReactNode;
text?: string;
error?: string;
}
function Form({ children, text, error }: FormProps) {
const theme = useTheme();
return (
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2} alignItems='center' style={{ height: '95vh' }}>
<Grid
container
spacing={2}
alignItems="center"
style={{ height: "95vh" }}
>
<Grid item xs={0.5} sm={2} md={4} />
<Grid item xs={11} sm={8} md={4}>
<Card
sx={{
...theme.typography.body2,
padding: '2vh',
textAlign: 'center',
padding: "2vh",
textAlign: "center",
color: theme.palette.text.secondary,
}}
>
<CardContent>
{text && (
<Typography
variant='h5'
sx={{ margin: '1vh 0vh', fontWeight: 'bold' }}
variant="h5"
sx={{
margin: "1vh 0vh",
fontWeight: "bold",
}}
>
{text}
</Typography>
)}
{children}
<Outlet />
{error && <Alert severity='error'>{error}</Alert>}
{error && <Alert severity="error">{error}</Alert>}
</CardContent>
</Card>
</Grid>

View file

@ -1,56 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Checkbox, FormControlLabel, Typography } from '@mui/material';
import { Form } from '.';
import { FormSubmit, FormTextField } from './StyledElements';
function Login({ form, handleSubmit, handleChange, handleCheckboxChange, error }) {
return (
<Form text='Login' error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label='Email'
name='email'
onChange={handleChange}
value={form.email}
/>
<FormTextField
required
label='Password'
name='password'
onChange={handleChange}
value={form.password}
other={{ type: 'password' }}
/>
<div
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
}}
>
<FormControlLabel
control={<Checkbox checked={form.remember} />}
labelPlacement='start'
label='Remember me'
name='remember'
onChange={handleCheckboxChange}
/>
</div>
<FormSubmit color='primary' type='submit'>
Login
</FormSubmit>
<Typography variant='body1'>
Dont have an account? <Link to='/register'>Register</Link>
</Typography>
<Typography variant='body1'>
Forgot your password? <Link to='/recover'>Recover Password</Link>
</Typography>
</form>
</Form>
);
}
export default Login;

View file

@ -0,0 +1,77 @@
import { Checkbox, FormControlLabel, Typography } from "@mui/material";
import { Link } from "react-router-dom";
import Form from "./Form";
import { FormSubmit, FormTextField } from "./StyledElements";
interface LoginProps {
form: {
email: string;
password: string;
remember: boolean;
};
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
handleCheckboxChange: (event: React.SyntheticEvent<Element, Event>) => void;
error: string;
}
function Login({
form,
handleSubmit,
handleChange,
handleCheckboxChange,
error,
}: LoginProps) {
return (
<Form text="Login" error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label="Email"
name="email"
onChange={handleChange}
value={form.email}
/>
<FormTextField
required
label="Password"
name="password"
onChange={handleChange}
value={form.password}
other={{ type: "password" }}
/>
<div
style={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
}}
>
<FormControlLabel
control={<Checkbox checked={form.remember} />}
labelPlacement="start"
label="Remember me"
name="remember"
onChange={handleCheckboxChange}
/>
</div>
<FormSubmit color="primary" type="submit">
Login
</FormSubmit>
<Typography variant="body1">
Dont have an account?
{" "}
<Link to="/register">Register</Link>
</Typography>
<Typography variant="body1">
Forgot your password?
{" "}
<Link to="/recover">Recover Password</Link>
</Typography>
</form>
</Form>
);
}
export default Login;

View file

@ -1,58 +1,88 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Box, Grid, Stack, Typography } from '@mui/material';
import {
Box, Grid, Stack, Typography,
} from "@mui/material";
import { Link } from "react-router-dom";
import OrgUnit from './OrgUnit';
import StackItem from './StackItem';
import { Form, FormButton, FormField } from './StyledElements';
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' };
import { Organization } from "../types";
interface MainPageProps {
form: { id: string; name: string };
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
orgs: Organization[];
ownedOrgs: Organization[];
joinOrg: (event: React.FormEvent<HTMLFormElement>) => void;
createOrg: (event: React.FormEvent<HTMLFormElement>) => void;
}
export default function MainPage({
form,
handleChange,
orgs,
ownedOrgs,
joinOrg,
createOrg,
}: MainPageProps) {
const linkStyle = { textDecoration: "none", color: "inherit" };
return (
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2} alignItems='center' style={{ height: '95vh' }}>
<Grid
container
spacing={2}
alignItems="center"
style={{ height: "95vh" }}
>
<Grid item xs={0.5} sm={2} md={4} />
<Grid item xs={11} sm={8} md={4}>
<Stack spacing={2}>
<StackItem text='Join an Organization'>
<StackItem text="Join an Organization">
<form onSubmit={joinOrg}>
<Form>
<FormField
label='Organization ID'
name='id'
label="Organization ID"
name="id"
value={form.id}
onChange={handleChange}
/>
<FormButton type='submit' variant='outlined'>
<FormButton
type="submit"
variant="outlined"
>
Join Organization
</FormButton>
</Form>
</form>
</StackItem>
<StackItem text='Create an Organization'>
<StackItem text="Create an Organization">
<form onSubmit={createOrg}>
<Form>
<FormField
label='Organization Name'
name='name'
label="Organization Name"
name="name"
value={form.name}
onChange={handleChange}
/>
<FormButton type='submit' variant='outlined'>
<FormButton
type="submit"
variant="outlined"
>
Create Organization
</FormButton>
</Form>
</form>
</StackItem>
<StackItem>
<Typography variant='h6'>
<Link to='org' style={linkStyle}>
<Typography variant="h6">
<Link to="org" style={linkStyle}>
Your Organizations
</Link>
</Typography>
{ownedOrgs.length === 0 && (
<Typography variant='body2'>
<Typography variant="body2">
You do own any organizations
</Typography>
)}
@ -62,13 +92,13 @@ function MainPageBase({ form, orgs, ownedOrgs, joinOrg, createOrg, handleChange
))}
</StackItem>
<StackItem>
<Typography variant='h6'>
<Link to='org/joined' style={linkStyle}>
<Typography variant="h6">
<Link to="org/joined" style={linkStyle}>
Joined Organizations
</Link>
</Typography>
{orgs.length === 0 && (
<Typography variant='body2'>
<Typography variant="body2">
You are not a member in any organizations
</Typography>
)}
@ -84,5 +114,3 @@ function MainPageBase({ form, orgs, ownedOrgs, joinOrg, createOrg, handleChange
</Box>
);
}
export default MainPageBase;

View file

@ -1,47 +0,0 @@
import React from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Grid,
IconButton,
Typography,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
function MemberUnit({ member, onClick }) {
const { id, firstName, lastName, nickname, pronouns, email } = member;
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>
{firstName} {lastName} ({nickname}): {pronouns}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={1}>
<Grid item textAlign='left' xs={11} sm={11}>
<Typography variant='body1'>ID: {id}</Typography>
<Typography variant='body1'>Email: {email}</Typography>
</Grid>
<Grid item xs={1} sm={1} justifyContent='flex-end'>
<IconButton
size='large'
edge='start'
color='inherit'
aria-label='menu'
sx={{ mr: 2 }}
onClick={() => onClick(member.id)}
>
<PlayArrowIcon />
</IconButton>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
);
}
export default MemberUnit;

View file

@ -0,0 +1,57 @@
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Grid,
IconButton,
Typography,
} from "@mui/material";
import { User } from "../types";
interface MemberUnitProps {
member: User;
onClick: (id: number) => void;
}
export default function MemberUnit({ member, onClick }: MemberUnitProps) {
const { id, firstName, lastName, nickname, pronouns, email } = member;
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>
{firstName} {lastName} ({nickname}
): {pronouns}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Grid container spacing={1}>
<Grid item textAlign="left" xs={11} sm={11}>
<Typography variant="body1">
ID:
{id}
</Typography>
<Typography variant="body1">
Email:
{email}
</Typography>
</Grid>
<Grid item xs={1} sm={1} justifyContent="flex-end">
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
onClick={() => onClick(member.id)}
>
<PlayArrowIcon />
</IconButton>
</Grid>
</Grid>
</AccordionDetails>
</Accordion>
);
}

View file

@ -1,91 +0,0 @@
import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import LoginIcon from '@mui/icons-material/Login';
import LogoutIcon from '@mui/icons-material/Logout';
import MailIcon from '@mui/icons-material/Mail';
import SearchIcon from '@mui/icons-material/Search';
import MenuIcon from '@mui/icons-material/Menu';
import {
AppBar,
Box,
Button,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Toolbar,
Typography,
} from '@mui/material';
import { Link } from 'react-router-dom';
export default function MobileNavbar({ logout, userData, open, setOpen, mode, toggleMode }) {
function ControlButtons() {
return userData.username ? (
<ListItem>
<ListItemButton color='inherit' component={Link} to='/login' onClick={logout}>
<ListItemIcon>
<LogoutIcon />
</ListItemIcon>
<ListItemText>Logout</ListItemText>
</ListItemButton>
</ListItem>
) : (
<>
<ListItem>
<ListItemButton color='inherit' component={Link} to='/login'>
<ListItemIcon>
<LoginIcon />
</ListItemIcon>
<ListItemText>Login</ListItemText>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton color='inherit' component={Link} to='/register'>
<ListItemIcon>
<AppRegistrationIcon />
</ListItemIcon>
<ListItemText>Register</ListItemText>
</ListItemButton>
</ListItem>
</>
);
}
return (
<AppBar position='static'>
<Toolbar>
<IconButton component={Link} to='/' sx={{ margin: '0 1vw 0 0 ' }}>
<MenuIcon />
</IconButton>
<Typography variant='h6' component='div'>
KnowItAll
</Typography>
<Box sx={{ flexGrow: 1 }} />
<Button color='inherit' onClick={setOpen}>
Menu
</Button>
</Toolbar>
<Drawer anchor='right' open={open} onClose={setOpen}>
<List>
<ControlButtons />
<ListItem>
<ListItemButton onClick={toggleMode}>
<ListItemIcon>
{mode === 'light' ? <DarkModeIcon /> : <LightModeIcon />}
</ListItemIcon>
<ListItemText>
Switch to {mode === 'light' ? 'dark' : 'light'} mode
</ListItemText>
</ListItemButton>
</ListItem>
</List>
</Drawer>
</AppBar>
);
}

View file

@ -0,0 +1,125 @@
import AppRegistrationIcon from "@mui/icons-material/AppRegistration";
import DarkModeIcon from "@mui/icons-material/DarkMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import LoginIcon from "@mui/icons-material/Login";
import LogoutIcon from "@mui/icons-material/Logout";
import MenuIcon from "@mui/icons-material/Menu";
import {
AppBar,
Box,
Button,
Drawer,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Toolbar,
Typography,
} from "@mui/material";
import { Link } from "react-router-dom";
import { User, ThemeMode } from "../types";
interface ControlButtonsProps {
userData: User;
logout: () => void;
}
function ControlButtons({ userData, logout }: ControlButtonsProps) {
return userData.username ? (
<ListItem>
<ListItemButton
color="inherit"
component={Link}
to="/login"
onClick={logout}
>
<ListItemIcon>
<LogoutIcon />
</ListItemIcon>
<ListItemText>Logout</ListItemText>
</ListItemButton>
</ListItem>
) : (
<>
<ListItem>
<ListItemButton color="inherit" component={Link} to="/login">
<ListItemIcon>
<LoginIcon />
</ListItemIcon>
<ListItemText>Login</ListItemText>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton color="inherit" component={Link} to="/register">
<ListItemIcon>
<AppRegistrationIcon />
</ListItemIcon>
<ListItemText>Register</ListItemText>
</ListItemButton>
</ListItem>
</>
);
}
interface MobileNavbarProps {
userData: User;
logout: () => void;
open: boolean;
toggleOpen: () => void;
mode: ThemeMode;
toggleMode: () => void;
}
export default function MobileNavbar({
logout,
userData,
open,
toggleOpen,
mode,
toggleMode,
}: MobileNavbarProps) {
return (
<AppBar position="static">
<Toolbar>
<IconButton
component={Link}
to="/"
sx={{ margin: "0 1vw 0 0 " }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div">
KnowItAll
</Typography>
<Box sx={{ flexGrow: 1 }} />
<Button color="inherit" onClick={toggleOpen}>
Menu
</Button>
</Toolbar>
<Drawer anchor="right" open={open} onClose={toggleOpen}>
<List>
<ControlButtons userData={userData} logout={logout} />
<ListItem>
<ListItemButton onClick={toggleMode}>
<ListItemIcon>
{mode === "light" ? (
<DarkModeIcon />
) : (
<LightModeIcon />
)}
</ListItemIcon>
<ListItemText>
Switch to {mode === "light" ? "dark" : "light"}{" "}
mode
</ListItemText>
</ListItemButton>
</ListItem>
</List>
</Drawer>
</AppBar>
);
}

View file

@ -1,57 +0,0 @@
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import MenuIcon from '@mui/icons-material/Menu';
import { AppBar, Box, Button, IconButton, Toolbar, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
export default function Navbar({ logout, userData, mode, toggleMode }) {
function NavbarControls() {
return userData.email ? (
<>
<Button color='inherit' component={Link} to='/update'>
Account
</Button>
<Button
color='inherit'
component={Link}
to='/login'
onClick={logout}
sx={{ margin: '0 1vh' }}
>
Logout
</Button>
</>
) : (
<>
<Button color='inherit' component={Link} to='/login'>
Login
</Button>
<Button color='inherit' component={Link} to='/register'>
Register
</Button>
</>
);
}
return (
<AppBar position='static'>
<Toolbar>
<IconButton
color='inherit'
component={Link}
to='/'
sx={{ margin: '1vh 1vw 1vh 0' }}
>
<MenuIcon />
</IconButton>
<Typography variant='h6' component='div'>
KnowItAll
</Typography>
<Box sx={{ flexGrow: 1 }} />
<IconButton color='inherit' onClick={toggleMode} sx={{ margin: '0 1vh' }}>
{mode === 'light' ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton>
<NavbarControls />
</Toolbar>
</AppBar>
);
}

View file

@ -0,0 +1,88 @@
import DarkModeIcon from "@mui/icons-material/DarkMode";
import LightModeIcon from "@mui/icons-material/LightMode";
import MenuIcon from "@mui/icons-material/Menu";
import {
AppBar,
Box,
Button,
IconButton,
Toolbar,
Typography,
} from "@mui/material";
import { Link } from "react-router-dom";
import { User, ThemeMode } from "../types";
interface NavbarControlsProps {
userData: User;
logout: () => void;
}
function NavbarControls({ userData, logout }: NavbarControlsProps) {
return userData.email ? (
<>
<Button color="inherit" component={Link} to="/update">
Account
</Button>
<Button
color="inherit"
component={Link}
to="/login"
onClick={logout}
sx={{ margin: "0 1vh" }}
>
Logout
</Button>
</>
) : (
<>
<Button color="inherit" component={Link} to="/login">
Login
</Button>
<Button color="inherit" component={Link} to="/register">
Register
</Button>
</>
);
}
interface NavbarProps {
userData: User;
logout: () => void;
mode: ThemeMode;
toggleMode: () => void;
}
export default function Navbar({
userData,
logout,
mode,
toggleMode,
}: NavbarProps) {
return (
<AppBar position="static">
<Toolbar>
<IconButton
color="inherit"
component={Link}
to="/"
sx={{ margin: "1vh 1vw 1vh 0" }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" component="div">
KnowItAll
</Typography>
<Box sx={{ flexGrow: 1 }} />
<IconButton
color="inherit"
onClick={toggleMode}
sx={{ margin: "0 1vh" }}
>
{mode === "light" ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton>
<NavbarControls userData={userData} logout={logout} />
</Toolbar>
</AppBar>
);
}

View file

@ -1,27 +0,0 @@
import { Link } from 'react-router-dom';
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
function OrgUnit({ org }) {
const { id, name, memberCount, createdAt } = org;
const linkStyle = { textDecoration: 'none', color: 'inherit' };
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>
<Link to={`/org/${id}`} style={linkStyle}>
{name}
</Link>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant='body1'>Members: {memberCount}</Typography>
<Typography variant='body1'>Created On: {createdAt}</Typography>
</AccordionDetails>
</Accordion>
);
}
export default OrgUnit;

View file

@ -0,0 +1,44 @@
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Typography,
} from "@mui/material";
import { Link } from "react-router-dom";
import { Organization } from "../types";
interface OrgUnitProps {
org: Organization;
}
function OrgUnit({ org }: OrgUnitProps) {
const { id, name, memberCount, createdAt } = org;
const linkStyle = { textDecoration: "none", color: "inherit" };
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>
<Link to={`/org/${id}`} style={linkStyle}>
{name}
</Link>
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant="body1">
Members:
{memberCount}
</Typography>
<Typography variant="body1">
Created On:
{createdAt}
</Typography>
</AccordionDetails>
</Accordion>
);
}
export default OrgUnit;

View file

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

View file

@ -0,0 +1,35 @@
import { Box, Paper, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import OrgUnit from "./OrgUnit";
import { Organization } from "../types";
interface OrganizationListProps {
orgs: Organization[];
}
export default function OrganizationList({ orgs }: OrganizationListProps) {
const theme = useTheme();
return (
<Paper
sx={{
...theme.typography.body2,
padding: "2vh",
color: theme.palette.text.secondary,
flexGrow: 1,
height: "89vh",
margin: "2vh",
}}
>
<Typography variant="h6">Organizations</Typography>
<hr />
<Box sx={{ overflowY: "auto" }}>
{orgs.map((org) => (
<OrgUnit org={org} key={org.id} />
))}
</Box>
</Paper>
);
}

View file

@ -1,35 +0,0 @@
import Mic from '@mui/icons-material/Mic';
import PlayArrow from '@mui/icons-material/PlayArrow';
import SaveAs from '@mui/icons-material/SaveAs';
import Upload from '@mui/icons-material/Upload';
import { IconButton } from '@mui/material';
function RecordMp3({ togglePlay, record, uploadRecord, handleSubmit }) {
return (
<form onSubmit={handleSubmit}>
<IconButton onClick={togglePlay}>
<PlayArrow />
</IconButton>
<IconButton onClick={record}>
<Mic />
</IconButton>
<input
id='icon-button-file'
type='file'
accept='audio/*'
hidden
onChange={uploadRecord}
/>
<label htmlFor='icon-button-file'>
<IconButton component='span'>
<Upload />
</IconButton>
</label>
<IconButton type='submit'>
<SaveAs />
</IconButton>
</form>
);
}
export default RecordMp3;

View file

@ -0,0 +1,47 @@
import Mic from "@mui/icons-material/Mic";
import PlayArrow from "@mui/icons-material/PlayArrow";
import SaveAs from "@mui/icons-material/SaveAs";
import Upload from "@mui/icons-material/Upload";
import { IconButton } from "@mui/material";
interface RecordMp3Props {
togglePlay: () => void;
record: () => void;
uploadRecord: (event: React.ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
}
function RecordMp3({
togglePlay,
record,
uploadRecord,
handleSubmit,
}: RecordMp3Props) {
return (
<form onSubmit={handleSubmit}>
<IconButton onClick={togglePlay}>
<PlayArrow />
</IconButton>
<IconButton onClick={record}>
<Mic />
</IconButton>
<input
id="icon-button-file"
type="file"
accept="audio/*"
hidden
onChange={uploadRecord}
/>
<label htmlFor="icon-button-file">
<IconButton component="span">
<Upload />
</IconButton>
</label>
<IconButton type="submit">
<SaveAs />
</IconButton>
</form>
);
}
export default RecordMp3;

View file

@ -1,23 +0,0 @@
import { Form } from '.';
import { FormSubmit, FormTextField } from './StyledElements';
function RecoverForm({ form, error, handleSubmit, handleChange }) {
return (
<Form text='Recover Password' error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label='Email'
name='email'
onChange={handleChange}
value={form.email}
/>
<FormSubmit color='primary' type='submit'>
Recover my password
</FormSubmit>
</form>
</Form>
);
}
export default RecoverForm;

View file

@ -0,0 +1,37 @@
import Form from "./Form";
import { FormSubmit, FormTextField } from "./StyledElements";
interface RecoverFormProps {
form: {
email: string;
};
error: string;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
function RecoverForm({
form,
error,
handleSubmit,
handleChange,
}: RecoverFormProps) {
return (
<Form text="Recover Password" error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label="Email"
name="email"
onChange={handleChange}
value={form.email}
/>
<FormSubmit color="primary" type="submit">
Recover my password
</FormSubmit>
</form>
</Form>
);
}
export default RecoverForm;

View file

@ -1,70 +0,0 @@
import { Link } from 'react-router-dom';
import { Box, Typography } from '@mui/material';
import Form from './Form';
import { FormSubmit, FormTextField } from './StyledElements';
function RegisterForm({ form, handleSubmit, handleChange, error }) {
return (
<Form text='Create an Account' error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label='First Name'
name='firstName'
onChange={handleChange}
value={form.firstName}
/>
<FormTextField
required
label='Last Name'
name='lastName'
onChange={handleChange}
value={form.lastName}
/>
<FormTextField
required
label='Email'
name='email'
onChange={handleChange}
value={form.email}
/>
<FormTextField
fullWidth
label='Pronouns'
name='pronouns'
onChange={handleChange}
value={form.pronouns}
/>
<FormTextField
required
label='Password'
name='password'
onChange={handleChange}
value={form.password}
other={{ type: 'password' }}
/>
<FormTextField
required
label='Confirm Password'
name='confirmPassword'
onChange={handleChange}
value={form.confirmPassword}
other={{ type: 'password' }}
/>
<FormSubmit
color='primary'
type='submit'
other={{ disabled: form.password !== form.confirmPassword }}
>
Create an Account
</FormSubmit>
<Typography variant='body1'>
Already have an account? <Link to='/login'>Login</Link>
</Typography>
</form>
</Form>
);
}
export default RegisterForm;

View file

@ -0,0 +1,90 @@
import { Typography } from "@mui/material";
import { Link } from "react-router-dom";
import Form from "./Form";
import { FormSubmit, FormTextField } from "./StyledElements";
interface RegisterFormProps {
form: {
firstName: string;
lastName: string;
email: string;
pronouns: string;
password: string;
confirmPassword: string;
};
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
error: string;
}
function RegisterForm({
form,
handleSubmit,
handleChange,
error,
}: RegisterFormProps) {
return (
<Form text="Create an Account" error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label="First Name"
name="firstName"
onChange={handleChange}
value={form.firstName}
/>
<FormTextField
required
label="Last Name"
name="lastName"
onChange={handleChange}
value={form.lastName}
/>
<FormTextField
required
label="Email"
name="email"
onChange={handleChange}
value={form.email}
/>
<FormTextField
label="Pronouns"
name="pronouns"
onChange={handleChange}
value={form.pronouns}
/>
<FormTextField
required
label="Password"
name="password"
onChange={handleChange}
value={form.password}
other={{ type: "password" }}
/>
<FormTextField
required
label="Confirm Password"
name="confirmPassword"
onChange={handleChange}
value={form.confirmPassword}
other={{ type: "password" }}
/>
<FormSubmit
color="primary"
type="submit"
other={{ disabled: form.password !== form.confirmPassword }}
>
Create an Account
</FormSubmit>
<Typography variant="body1">
Already have an account?
{" "}
<Link to="/login">Login</Link>
</Typography>
</form>
</Form>
);
}
export default RegisterForm;

View file

@ -1,36 +0,0 @@
import { Form } from '.';
import { FormSubmit, FormTextField } from './StyledElements';
function ResetForm({ form, handleSubmit, handleChange, error }) {
return (
<Form text='Reset Password' error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label='Password'
name='password'
onChange={handleChange}
value={form.password}
other={{ type: 'password' }}
/>
<FormTextField
required
label='Confirm Password'
name='confirmPassword'
onChange={handleChange}
value={form.confirmPassword}
other={{ type: 'password' }}
/>
<FormSubmit
color='primary'
type='submit'
other={{ disabled: form.password !== form.confirmPassword }}
>
Reset my Password
</FormSubmit>
</form>
</Form>
);
}
export default ResetForm;

View file

@ -0,0 +1,51 @@
import Form from "./Form";
import { FormSubmit, FormTextField } from "./StyledElements";
interface ResetFormProps {
form: {
password: string;
confirmPassword: string;
};
error: string;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}
function ResetForm({
form,
handleSubmit,
handleChange,
error,
}: ResetFormProps) {
return (
<Form text="Reset Password" error={error}>
<form onSubmit={handleSubmit}>
<FormTextField
required
label="Password"
name="password"
onChange={handleChange}
value={form.password}
other={{ type: "password" }}
/>
<FormTextField
required
label="Confirm Password"
name="confirmPassword"
onChange={handleChange}
value={form.confirmPassword}
other={{ type: "password" }}
/>
<FormSubmit
color="primary"
type="submit"
other={{ disabled: form.password !== form.confirmPassword }}
>
Reset my Password
</FormSubmit>
</form>
</Form>
);
}
export default ResetForm;

View file

@ -1,21 +0,0 @@
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

@ -0,0 +1,26 @@
import { Paper, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
interface StackItemProps {
text?: string;
children: React.ReactNode;
}
function StackItem({ text, children }: StackItemProps) {
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

@ -1,80 +0,0 @@
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 (
<TextField
required={required}
fullWidth
label={label}
name={name}
variant='outlined'
onChange={onChange}
value={value}
sx={{ margin: '1vh 0vh' }}
{...other}
/>
);
};
const FormSubmit = ({ color, children, type, other }) => {
return (
<Button
variant='contained'
color={color}
sx={{ margin: '1vh 0vh' }}
type={type}
fullWidth
{...other}
>
{children}
</Button>
);
};
export { Form, FormField, FormButton, DynamicStack, DynamicPaper, FormSubmit, FormTextField };

View file

@ -0,0 +1,119 @@
import {
Button,
FormGroup,
Paper,
Stack,
TextField,
TextFieldProps,
ButtonProps,
} 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",
},
}));
interface FormTextFieldProps {
label: string;
name: string;
value: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
other?: TextFieldProps;
}
function FormTextField({
label,
name,
value,
onChange,
required,
other,
}: FormTextFieldProps) {
return (
<TextField
required={required}
fullWidth
label={label}
name={name}
variant="outlined"
onChange={onChange}
value={value}
sx={{ margin: "1vh 0vh" }}
{...other}
/>
);
}
interface FormSubmitProps {
color: "primary" | "secondary" | "error";
children: React.ReactNode;
type: "button" | "submit";
other?: ButtonProps;
}
function FormSubmit({ color, children, type, other }: FormSubmitProps) {
return (
<Button
variant="contained"
color={color}
sx={{ margin: "1vh 0vh" }}
type={type}
fullWidth
{...other}
>
{children}
</Button>
);
}
export {
DynamicPaper,
DynamicStack,
Form,
FormButton,
FormField,
FormSubmit,
FormTextField,
};

View file

@ -1,23 +0,0 @@
import { Form } from '.';
import { FormSubmit, FormTextField } from './StyledElements';
function UpdateOrganizationForm({ name, handleSubmit, handleChange }) {
return (
<Form text='Rename Organization'>
<form onSubmit={handleSubmit}>
<FormTextField
required
label='Name'
name='name'
onChange={handleChange}
value={name}
/>
<FormSubmit color='primary' type='submit'>
Rename
</FormSubmit>
</form>
</Form>
);
}
export default UpdateOrganizationForm;

View file

@ -0,0 +1,31 @@
import Form from "./Form";
import { FormSubmit, FormTextField } from "./StyledElements";
interface UpdateOrganizationFormProps {
name: string;
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
}
export default function UpdateOrganizationForm({
name,
handleSubmit,
handleChange,
}: UpdateOrganizationFormProps) {
return (
<Form text="Rename Organization">
<form onSubmit={handleSubmit}>
<FormTextField
required
label="Name"
name="name"
onChange={handleChange}
value={name}
/>
<FormSubmit color="primary" type="submit">
Rename
</FormSubmit>
</form>
</Form>
);
}

View file

@ -1,33 +0,0 @@
import Navbar from './Navbar';
import MobileNavbar from './MobileNavbar';
import LoginForm from './LoginForm';
import MainPageBase from './MainPageBase';
import EditUserForm from './EditUserForm';
import Form from './Form';
import OrgUnit from './OrgUnit';
import StackItem from './StackItem';
import RecordMp3 from './RecordMp3';
import RecoverForm from './RecoverForm';
import ResetForm from './ResetForm';
import OrganizationList from './OrganizationList';
import MemberUnit from './MemberUnit';
import Dashboard from './Dashboard';
import UpdateOrganizationForm from './UpdateOrganizationForm';
export {
Navbar,
MobileNavbar,
LoginForm,
MainPageBase,
EditUserForm,
Form,
OrgUnit,
StackItem,
RecordMp3,
RecoverForm,
ResetForm,
OrganizationList,
MemberUnit,
Dashboard,
UpdateOrganizationForm,
};

View file

@ -0,0 +1,27 @@
import Navbar from "./Navbar";
import MobileNavbar from "./MobileNavbar";
import MainPage from "./MainPage";
import LoginForm from "./LoginForm";
import RegisterForm from "./RegisterForm";
import EditUserForm from "./EditUserForm";
import RecordMp3 from "./RecordMp3";
import RecoverForm from "./RecoverForm";
import ResetForm from "./ResetForm";
import OrganizationList from "./OrganizationList";
import Dashboard from "./Dashboard";
import UpdateOrganizationForm from "./UpdateOrganizationForm";
export {
Navbar,
MobileNavbar,
MainPage,
LoginForm,
RegisterForm,
EditUserForm,
RecordMp3,
RecoverForm,
ResetForm,
OrganizationList,
Dashboard,
UpdateOrganizationForm,
};

View file

@ -1,6 +1,6 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { EditUserForm } from '../controllers';
import { EditUserForm } from "../controllers";
function Login() {
return (

View file

@ -1,10 +1,20 @@
import { Box, CssBaseline } from '@mui/material';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { Helmet } from 'react-helmet';
import { Outlet } from 'react-router-dom';
import { Box, CssBaseline } from "@mui/material";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { Helmet } from "react-helmet";
import { Outlet } from "react-router-dom";
import { useMode } from '../contexts';
import { Navbar } from '../controllers';
import { useMode } from "../contexts";
import { Navbar } from "../controllers";
declare module "@mui/material/styles" {
interface BreakpointOverrides {
xs: true;
sm: true;
md: true;
lg: false;
xl: false;
}
}
function Layout() {
const { mode } = useMode();
@ -28,7 +38,7 @@ function Layout() {
</Helmet>
<main>
<Navbar />
<Box sx={{ height: { xs: '', sm: '92vh', md: '92vh' } }}>
<Box sx={{ height: { xs: "", sm: "92vh", md: "92vh" } }}>
<Outlet />
</Box>
</main>

View file

@ -1,6 +1,6 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { LoginForm } from '../controllers';
import { LoginForm } from "../controllers";
function Login() {
return (

View file

@ -1,27 +0,0 @@
import Cookies from 'js-cookie';
import { useEffect } from 'react';
import Helmet from 'react-helmet';
import { useNavigate } from 'react-router-dom';
import { MainPageDisplay } from '../controllers';
function MainPage() {
const navigate = useNavigate();
useEffect(() => {
if (!Cookies.get('token')) {
navigate('/login');
}
}, []);
return (
<>
<Helmet>
<title>KnowItAll</title>
</Helmet>
<MainPageDisplay />
</>
);
}
export default MainPage;

View file

@ -0,0 +1,14 @@
import Helmet from "react-helmet";
import { MainPage as MainPageController } from "../controllers";
export default function MainPage() {
return (
<>
<Helmet>
<title>KnowItAll</title>
</Helmet>
<MainPageController />
</>
);
}

View file

@ -1,8 +1,8 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { MemberOrgsDashboard } from '../controllers';
import { MemberOrgsDashboard } from "../controllers";
function MemberOrgs() {
export default function MemberOrgs() {
return (
<>
<Helmet>
@ -12,5 +12,3 @@ function MemberOrgs() {
</>
);
}
export default MemberOrgs;

View file

@ -1,8 +1,8 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { Dashboard } from '../controllers';
import { Dashboard } from "../controllers";
function OrgDashboard() {
export default function OrgDashboard() {
return (
<>
<Helmet>
@ -12,5 +12,3 @@ function OrgDashboard() {
</>
);
}
export default OrgDashboard;

View file

@ -1,6 +1,6 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { OwnedOrgsDashboard } from '../controllers';
import { OwnedOrgsDashboard } from "../controllers";
function OwnedOrgs() {
return (

View file

@ -1,6 +1,6 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { RecoverForm } from '../controllers';
import { RecoverForm } from "../controllers";
function Login() {
return (

View file

@ -1,6 +1,6 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { RegisterForm } from '../controllers';
import { RegisterForm } from "../controllers";
function Register() {
return (

View file

@ -1,8 +1,8 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { ResetForm } from '../controllers';
import { ResetForm } from "../controllers";
function Reset() {
export default function Reset() {
return (
<>
<Helmet>
@ -12,5 +12,3 @@ function Reset() {
</>
);
}
export default Reset;

View file

@ -1,8 +1,8 @@
import Helmet from 'react-helmet';
import Helmet from "react-helmet";
import { UpdateOrganizationForm } from '../controllers';
import { UpdateOrganizationForm } from "../controllers";
function UpdateOrganization() {
export default function UpdateOrganization() {
return (
<>
<Helmet>
@ -12,5 +12,3 @@ function UpdateOrganization() {
</>
);
}
export default UpdateOrganization;

View file

@ -1,25 +0,0 @@
import Layout from './Layout';
import Login from './Login';
import Register from './Register';
import MainPage from './MainPage';
import EditUser from './EditUser';
import Recover from './Recover';
import Reset from './Reset';
import OwnedOrgs from './OwnedOrgs';
import MemberOrgs from './MemberOrgs';
import OrgDashboard from './OrgDashboard';
import UpdateOrganization from './UpdateOrganization';
export {
Layout,
Login,
Register,
MainPage,
EditUser,
Recover,
Reset,
OwnedOrgs,
MemberOrgs,
OrgDashboard,
UpdateOrganization,
};

View file

@ -0,0 +1,25 @@
import Layout from "./Layout";
import MainPage from "./MainPage";
import Login from "./Login";
import Register from "./Register";
import EditUser from "./EditUser";
import Recover from "./Recover";
import Reset from "./Reset";
import OwnedOrgs from "./OwnedOrgs";
import MemberOrgs from "./MemberOrgs";
import OrgDashboard from "./OrgDashboard";
import UpdateOrganization from "./UpdateOrganization";
export {
Layout,
MainPage,
Login,
Register,
EditUser,
Recover,
Reset,
OwnedOrgs,
MemberOrgs,
OrgDashboard,
UpdateOrganization,
};

View file

@ -1,11 +0,0 @@
import { ErrorProvider, ModeProvider, UserProvider } from '.';
export default function ContextProvider({ children }) {
return (
<ModeProvider>
<ErrorProvider>
<UserProvider>{children}</UserProvider>
</ErrorProvider>
</ModeProvider>
);
}

View file

@ -0,0 +1,19 @@
import { ReactNode } from "react";
import ErrorProvider from "./ErrorProvider";
import ModeProvider from "./ModeProvider";
import UserProvider from "./UserProvider";
interface ContextProviderProps {
children: ReactNode;
}
export default function ContextProvider({ children }: ContextProviderProps) {
return (
<ModeProvider>
<ErrorProvider>
<UserProvider>{children}</UserProvider>
</ErrorProvider>
</ModeProvider>
);
}

View file

@ -1,11 +0,0 @@
import { createContext, useContext, useState } from 'react';
const ErrorContext = createContext();
export default function ModeProvider({ children }) {
const [error, setError] = useState('');
return <ErrorContext.Provider value={{ error, setError }}>{children}</ErrorContext.Provider>;
}
export const useError = () => useContext(ErrorContext);

View file

@ -0,0 +1,20 @@
import React, { createContext, useContext } from "react";
interface ErrorContextData {
error: string;
setError: React.Dispatch<React.SetStateAction<string>>;
}
const ErrorContext = createContext<ErrorContextData | undefined>(undefined);
export default ErrorContext;
export const useError = () => {
const context = useContext(ErrorContext);
if (!context) {
throw new Error("useError must be used within a ErrorProvider");
}
return context;
};

View file

@ -0,0 +1,22 @@
import { useMemo, useState } from "react";
import ErrorContext from "./ErrorContext";
interface ErrorProviderProps {
children: React.ReactNode;
}
export default function ErrorProvider({ children }: ErrorProviderProps) {
const [error, setError] = useState("");
const contextValue = useMemo(
() => ({ error, setError }),
[error, setError]
);
return (
<ErrorContext.Provider value={contextValue}>
{children}
</ErrorContext.Provider>
);
}

View file

@ -1,16 +0,0 @@
import { createContext, useContext, useState, useEffect } from 'react';
import Cookies from 'js-cookie';
const ModeContext = createContext();
export default function ModeProvider({ children }) {
const [mode, setMode] = useState(Cookies.get('theme') || 'light');
useEffect(() => {
Cookies.set('theme', mode);
}, [mode]);
return <ModeContext.Provider value={{ mode, setMode }}>{children}</ModeContext.Provider>;
}
export const useMode = () => useContext(ModeContext);

View file

@ -0,0 +1,22 @@
import React, { createContext, useContext } from "react";
import { ThemeMode } from "../types";
interface ModeContextData {
mode: ThemeMode;
setMode: React.Dispatch<React.SetStateAction<ThemeMode>>;
}
const ModeContext = createContext<ModeContextData | undefined>(undefined);
export default ModeContext;
export const useMode = () => {
const context = useContext(ModeContext);
if (!context) {
throw new Error("useMode must be used within a ModeProvider");
}
return context;
};

View file

@ -0,0 +1,27 @@
import Cookies from "js-cookie";
import { useEffect, useMemo, useState } from "react";
import { ThemeMode } from "../types";
import ModeContext from "./ModeContext";
interface ModeProviderProps {
children: React.ReactNode;
}
export default function ModeProvider({ children }: ModeProviderProps) {
const [mode, setMode] = useState<ThemeMode>(
(Cookies.get("theme") as ThemeMode) || "light"
);
const contextValue = useMemo(() => ({ mode, setMode }), [mode, setMode]);
useEffect(() => {
Cookies.set("theme", mode);
}, [mode]);
return (
<ModeContext.Provider value={contextValue}>
{children}
</ModeContext.Provider>
);
}

View file

@ -1,43 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { remember } from '../services/userServices';
const initialUserData = { loading: true };
const UserContext = createContext();
export default function UserProvider({ children }) {
const [userData, setUserData] = useState(initialUserData);
const navigate = useNavigate();
useEffect(() => {
const loadUserData = async () => {
const { user, error } = await remember();
if (error) {
navigate('/login');
Cookies.remove('token');
} else {
setUserData(user || { loading: false });
}
};
if (userData.loading) loadUserData();
}, [userData.loading]);
return (
<UserContext.Provider
value={{
userData,
setUserData,
}}
>
{children}
</UserContext.Provider>
);
}
export const useUser = () => useContext(UserContext);

View file

@ -0,0 +1,23 @@
import React, { createContext, useContext } from "react";
import { User } from "../types";
interface UserContextData {
loading: boolean;
userData: User;
setUserData: React.Dispatch<React.SetStateAction<User>>;
}
const UserContext = createContext<UserContextData | undefined>(undefined);
export default UserContext;
export const useUser = () => {
const context = useContext(UserContext);
if (!context) {
throw new Error("useUser must be used within a UserProvider");
}
return context;
};

View file

@ -0,0 +1,63 @@
import Cookies from "js-cookie";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import UserContext from "./UserContext";
import createRequest from "../utils/requests";
import { User } from "../types";
interface UserProviderProps {
children: React.ReactNode;
}
export default function UserProvider({ children }: UserProviderProps) {
const [userData, setUserData] = useState<User>({
id: -1,
firstName: "",
lastName: "",
pronouns: "",
nickname: "",
username: "",
email: "",
age: 0,
});
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const contextValue = useMemo(
() => ({ loading, userData, setUserData }),
[loading, userData, setUserData]
);
const loadUserData = useCallback(async () => {
if (!loading) return;
if (!Cookies.get("token")) {
setLoading(() => false);
}
try {
const instance = createRequest();
const response = await instance.post("/auth/remember");
setUserData(() => response.data.user);
} catch (error) {
Cookies.remove("token");
navigate("/login");
} finally {
setLoading(() => false);
}
}, [navigate, loading]);
useEffect(() => {
loadUserData();
}, [loadUserData]);
return (
<UserContext.Provider value={contextValue}>
{children}
</UserContext.Provider>
);
}

View file

@ -1,6 +0,0 @@
import ModeProvider, { useMode } from './ModeContext';
import UserProvider, { useUser } from './UserContext';
import ErrorProvider, { useError } from './ErrorContext';
import ContextProvider from './ContextProvider';
export { ModeProvider, useMode, UserProvider, useUser, ErrorProvider, useError, ContextProvider };

View file

@ -0,0 +1,8 @@
import { useMode } from "./ModeContext";
import { useUser } from "./UserContext";
import { useError } from "./ErrorContext";
import ContextProvider from "./ContextProvider";
export {
useMode, useUser, useError, ContextProvider,
};

View file

@ -0,0 +1,113 @@
import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Dashboard as DashboardComponent } from "../components";
import createRequest from "../utils/requests";
export default function Dashboard() {
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 getOrg = useCallback(async () => {
try {
const instance = createRequest();
const response = await instance.get(`/org/${orgID}`);
const orgData = {
...response.data.org,
memberCount: response.data.memberCount,
};
setOrg(() => orgData);
setRows(() => response.data.org.member);
setStatus(() => response.data.owner);
} catch (error) {
navigate("/");
}
}, [navigate, orgID]);
useEffect(() => {
getOrg();
}, [getOrg]);
const handleOpen = useCallback(() => {
setOpen((initial) => !initial);
}, []);
const handleDelete = useCallback(async () => {
const instance = createRequest();
await instance.delete(`/org/${orgID}`);
navigate("/");
}, [navigate, orgID]);
const copyID = useCallback(() => {
navigator.clipboard.writeText(orgID);
}, [orgID]);
const copyJoinLink = useCallback(() => {
navigator.clipboard.writeText(
`${process.env.REACT_APP_DOMAIN_ROOT}/register?orgID=${orgID}`
);
}, [orgID]);
const removeSelected = useCallback(async () => {
const doomedUserIDs = selection.map((row) => row.id);
const instance = createRequest();
await instance.post(`/org/${orgID}/remove`, { doomedUserIDs });
}, [orgID, selection]);
const leaveOrg = useCallback(async () => {
const instance = createRequest();
await instance.post(`/org/${orgID}/remove`, {});
navigate("/");
}, [navigate, orgID]);
const play = useCallback(async (id) => {
const request = await axios.get(
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${id}.mp3`,
{ responseType: "blob" }
);
const uploadedFile = new File([request.data], "userAudio.mp3", {
type: request.data.type,
lastModified: Date.now(),
});
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
audioFile.play();
}, []);
return (
<DashboardComponent
org={org}
rows={rows}
open={open}
handleOpen={handleOpen}
setSelection={setSelection}
onClick={play}
status={status}
handleDelete={handleDelete}
copyID={copyID}
copyJoinLink={copyJoinLink}
removeSelected={removeSelected}
leaveOrg={leaveOrg}
/>
);
}

View file

@ -1,106 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Dashboard } from '../components';
import { getAudio } from '../services/userServices';
import { getOrg, deleteOrg, removeMember } from '../services/orgServices';
function DashboardController() {
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);
useEffect(() => {
async function getData() {
await getOrg(orgID)
.then((res) => {
setOrg(() => {
return { ...res.org, memberCount: res.memberCount };
});
setRows(() => res.org.member);
setStatus(() => res.owner);
})
.catch((e) => {
console.log(e);
navigate('/');
});
}
getData();
}, [navigate, orgID]);
function handleOpen() {
setOpen((initial) => (initial ? false : true));
}
async function handleDelete() {
await deleteOrg(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 removeMember(orgID, doomedUserIDs);
}
async function leaveOrg() {
await removeMember(orgID, {}).then(navigate('/'));
}
async function play(id) {
await getAudio(id).then((res) => {
const uploadedFile = new File([res], 'userAudio.mp3', {
type: res.type,
lastModified: Date.now(),
});
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
audioFile.play();
});
}
return (
<Dashboard
org={org}
rows={rows}
open={open}
handleOpen={handleOpen}
setSelection={setSelection}
onClick={play}
status={status}
handleDelete={handleDelete}
copyID={copyID}
copyJoinLink={copyJoinLink}
removeSelected={removeSelected}
leaveOrg={leaveOrg}
/>
);
}
export default DashboardController;

View file

@ -0,0 +1,67 @@
import { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { AxiosError } from "axios";
import { EditUserForm } from "../components";
import { useUser, useError } from "../contexts";
import createRequest from "../utils/requests";
function EditUserFormController() {
const navigate = useNavigate();
const { userData } = useUser();
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
pronouns: "",
});
const { error } = useError();
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
const { value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
},
[]
);
const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const instance = createRequest();
await instance.post("/auth/update", form);
navigate("/");
} catch (e) {
const error = e as AxiosError;
if (error.response?.status === 401) navigate("/login");
}
},
[form, navigate]
);
useEffect(() => {
setForm(() => ({
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
pronouns: userData.pronouns,
}));
}, [userData]);
return (
<EditUserForm
form={form}
error={error}
handleSubmit={handleSubmit}
handleChange={handleChange}
/>
);
}
export default EditUserFormController;

View file

@ -1,62 +0,0 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { EditUserForm } from '../components';
import { useUser } from '../contexts';
import { updateUserDetails } from '../services/userServices';
function EditUserFormController() {
const navigate = useNavigate();
const { userData } = useUser();
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
pronouns: '',
});
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 { error } = await updateUserDetails(form);
if (error) {
setError(() => error);
} else {
navigate('/');
}
}
useEffect(() => {
if (!userData.loading) {
setForm(() => ({
firstName: userData.firstName,
lastName: userData.lastName,
email: userData.email,
pronouns: userData.pronouns,
}));
}
}, [userData]);
return (
<EditUserForm
form={form}
error={error}
handleSubmit={handleSubmit}
handleChange={handleChange}
/>
);
}
export default EditUserFormController;

View file

@ -0,0 +1,89 @@
import { Buffer } from "buffer";
import Cookies from "js-cookie";
import React, { useCallback, useState } from "react";
import { useNavigate } from "react-router-dom";
import { LoginForm } from "../components";
import { useError, useUser } from "../contexts";
import createRequest from "../utils/requests";
import { APIError } from "../types";
function LoginFormController() {
const navigate = useNavigate();
const { error, setError } = useError();
const { setUserData } = useUser();
const [form, setForm] = useState({
email: "",
password: "",
remember: false,
});
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
const { value } = e.target;
setForm((prevForm) => ({ ...prevForm, [name]: value }));
},
[]
);
const handleCheckboxChange = useCallback(
(e: React.SyntheticEvent<Element, Event>) => {
const target = e.target as HTMLInputElement;
const { name } = target;
const { checked } = target;
setForm((prevForm) => ({ ...prevForm, [name]: checked }));
},
[]
);
const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const encoded = Buffer.from(
`${form.email}:${form.password}`
).toString("base64");
const instance = createRequest();
instance.defaults.headers.common.Authorization = `basic ${encoded}`;
try {
const response = await instance.post("/auth/login", {
remember: form.remember,
});
const expires = form.remember ? 365 : 1;
setUserData(() => response.data.user);
Cookies.set("token", response.data.token, { expires });
navigate("/");
} catch (e) {
const error = e as APIError;
if (error.response?.data?.error) {
setError(error.response.data.error);
} else {
setError("An error occurred");
}
}
},
[form, navigate, setUserData, setError]
);
return (
<LoginForm
form={form}
handleSubmit={handleSubmit}
handleChange={handleChange}
handleCheckboxChange={handleCheckboxChange}
error={error}
/>
);
}
export default LoginFormController;

View file

@ -1,51 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { LoginForm } from '../components';
import { useError } from '../contexts';
import { login } from '../services/userServices';
function LoginFormController({ setToken }) {
const navigate = useNavigate();
const { error, setError } = useError();
const [form, setForm] = useState({
email: '',
password: '',
remember: false,
});
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();
const { error } = await login(form);
if (error) {
setError(() => error);
} else {
navigate('/');
}
}
return <LoginForm {...{ form, handleSubmit, handleChange, handleCheckboxChange, error }} />;
}
export default LoginFormController;

View file

@ -0,0 +1,70 @@
import React, { useCallback, useEffect, useState } from "react";
import { MainPage as MainPageComponent } from "../components";
import createRequest from "../utils/requests";
import { Organization } from "../types";
export default function MainPage() {
const [ownedOrgs, setOwnedOrgs] = useState<Organization[]>([]);
const [orgs, setOrgs] = useState<Organization[]>([]);
const [form, setForm] = useState({
name: "",
id: "",
});
const getOwnedOrgs = useCallback(async () => {
const instance = createRequest();
const response = await instance.get("/auth/orgs");
setOwnedOrgs(() => response.data.orgs);
}, []);
const getMemberOrgs = useCallback(async () => {
const instance = createRequest();
const response = await instance.get("/auth/orgs/member");
setOrgs(() => response.data.orgs);
}, []);
useEffect(() => {
getOwnedOrgs();
getMemberOrgs();
}, [getOwnedOrgs, getMemberOrgs]);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
const { value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
},
[]
);
const makeOrg = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const instance = createRequest();
instance.post("/org/create", form).then(getOwnedOrgs);
},
[form, getOwnedOrgs]
);
const joinOrg = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const instance = createRequest();
instance.post(`/org/${form.id}/add`, {}).then(getMemberOrgs);
},
[form, getMemberOrgs]
);
return (
<MainPageComponent
form={form}
orgs={orgs}
ownedOrgs={ownedOrgs}
handleChange={handleChange}
joinOrg={joinOrg}
createOrg={makeOrg}
/>
);
}

View file

@ -1,75 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Cookies from 'js-cookie';
import { MainPageBase } from '../components';
import { getMemberOrgs, getOwnedOrgs } from '../services/userServices';
import { createOrg, addMember } from '../services/orgServices';
function MainPageController() {
const navigate = useNavigate();
const [ownedOrgs, setOwnedOrgs] = useState([]);
const [orgs, setOrgs] = useState([]);
const [form, setForm] = useState({
name: '',
id: '',
});
useEffect(() => {
if (Cookies.get('token')) {
retrieveMemberOrgs();
retrieveOwnedOrgs();
} 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 retrieveOwnedOrgs() {
const { orgs } = await getOwnedOrgs().catch((e) => console.log(e));
setOwnedOrgs(() => orgs);
}
async function retrieveMemberOrgs() {
const { orgs } = await getMemberOrgs().catch((e) => console.log(e));
setOrgs(() => orgs);
}
async function makeOrg(e) {
e.preventDefault();
await createOrg({ orgName: form.name })
.then(retrieveOwnedOrgs)
.catch((e) => console.log(e));
}
async function joinOrg(e) {
e.preventDefault();
await addMember.then(retrieveMemberOrgs).catch((e) => console.log(e));
}
return (
<MainPageBase
form={form}
orgs={orgs}
ownedOrgs={ownedOrgs}
handleChange={handleChange}
joinOrg={joinOrg}
createOrg={makeOrg}
/>
);
}
export default MainPageController;

View file

@ -1,20 +0,0 @@
import React, { useEffect, useState } from 'react';
import { OrganizationList } from '../components';
import { getMemberOrgs } from '../services/userServices';
function MemberOrgsDashboard() {
const [orgs, setOrgs] = useState([]);
async function getOrgs() {
getMemberOrgs().then((res) => {
setOrgs(() => res.orgs);
});
}
useEffect(getOrgs);
return <OrganizationList orgs={orgs} />;
}
export default MemberOrgsDashboard;

View file

@ -0,0 +1,21 @@
import { useCallback, useEffect, useState } from "react";
import { OrganizationList } from "../components";
import createRequest from "../utils/requests";
import { Organization } from "../types";
export default function MemberOrgsDashboard() {
const [orgs, setOrgs] = useState<Organization[]>([]);
const getOrgs = useCallback(async () => {
const instance = createRequest();
const response = await instance.get("/auth/orgs");
setOrgs(() => response.data.orgs);
}, []);
useEffect(() => {
getOrgs();
}, [getOrgs]);
return <OrganizationList orgs={orgs} />;
}

View file

@ -0,0 +1,61 @@
import { useMediaQuery, Theme } from "@mui/material";
import Cookies from "js-cookie";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { MobileNavbar, Navbar } from "../components";
import { useMode, useUser } from "../contexts";
import createRequest from "../utils/requests";
export default function NavbarController() {
const navigate = useNavigate();
const { userData, setUserData } = useUser();
const { mode, setMode } = useMode();
const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen((initial) => !initial);
const toggleMode = () => {
setMode(() => (mode === "light" ? "dark" : "light"));
};
const sm = useMediaQuery((theme: Theme) => theme.breakpoints.only("sm"));
const md = useMediaQuery((theme: Theme) => theme.breakpoints.only("md"));
const logoutUser = async () => {
try {
const instance = createRequest();
await instance.post("/auth/logout");
} finally {
Cookies.remove("token");
setUserData({
email: "",
firstName: "",
username: "",
age: 0,
lastName: "",
pronouns: "",
nickname: "",
id: 0,
});
navigate("/login");
}
};
return md || sm
? Navbar({
logout: logoutUser,
userData,
mode,
toggleMode,
})
: MobileNavbar({
logout: logoutUser,
userData,
open,
toggleOpen,
mode,
toggleMode,
});
}

View file

@ -1,44 +0,0 @@
import { useMediaQuery } from '@mui/material';
import Cookies from 'js-cookie';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { MobileNavbar, Navbar } from '../components';
import { useMode, useUser } from '../contexts';
import { logout } from '../services/userServices';
export default function NavbarController() {
const navigate = useNavigate();
const { userData, setUserData } = useUser();
const { mode, setMode } = useMode();
const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen((initial) => !initial);
const toggleMode = () => {
console.log('Changed mode');
setMode(() => (mode === 'light' ? 'dark' : 'light'));
};
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
const logoutUser = async () => {
await logout();
Cookies.remove('token');
setUserData({});
navigate('/login');
};
return md || sm
? Navbar({ logout: logoutUser, userData, mode, toggleMode })
: MobileNavbar({
logout: logoutUser,
userData,
open,
setOpen: toggleOpen,
mode,
toggleMode,
});
}

View file

@ -1,20 +0,0 @@
import React, { useEffect, useState } from 'react';
import { OrganizationList } from '../components';
import { getOwnedOrgs } from '../services/userServices';
function OwnedOrgsDashboard() {
const [orgs, setOrgs] = useState([]);
async function getOrgs() {
getOwnedOrgs().then((res) => {
setOrgs(() => res.orgs);
});
}
useEffect(getOrgs);
return <OrganizationList orgs={orgs} />;
}
export default OwnedOrgsDashboard;

View file

@ -0,0 +1,21 @@
import { useCallback, useEffect, useState } from "react";
import { OrganizationList } from "../components";
import { Organization } from "../types";
import createRequest from "../utils/requests";
export default function OwnedOrgsDashboard() {
const [orgs, setOrgs] = useState<Organization[]>([]);
const getOrgs = useCallback(async () => {
const instance = createRequest();
const response = await instance.get("/auth/orgs/member");
setOrgs(() => response.data.orgs);
}, []);
useEffect(() => {
getOrgs();
}, [getOrgs]);
return <OrganizationList orgs={orgs} />;
}

View file

@ -0,0 +1,103 @@
import axios from "axios";
import MicRecorder from "mic-recorder-to-mp3";
import { useCallback, useEffect, useState } from "react";
import { RecordMp3 as RecordMp3Component } from "../components";
import { useUser } from "../contexts";
import createRequest from "../utils/requests";
export default function RecordMp3() {
const { userData } = useUser();
const [recorder] = useState(
new MicRecorder({
bitRate: 128,
})
);
const [recording, setRecording] = useState(false);
const [uploadedFile, setUploadedFile] = useState();
const [audioRecording, setAudioRecording] = useState(null);
const getData = useCallback(async () => {
const response = await axios.get(
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${userData.id}.mp3`,
{ responseType: "blob" }
);
setUploadedFile(
() =>
new File([response.data], "userAudio.mp3", {
type: response.data.type,
lastModified: Date.now(),
})
);
}, [userData.id]);
useEffect(() => {
getData();
}, [getData]);
const togglePlay = useCallback(() => {
if (audioRecording && !audioRecording.ended) {
audioRecording.pause();
audioRecording.currentTime = 0;
setAudioRecording(() => null);
} else {
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
setAudioRecording(() => audioFile);
audioFile.play();
}
}, [audioRecording, uploadedFile]);
const record = useCallback(() => {
if (recording) {
setRecording(() => false);
recorder
.stop()
.getMp3()
.then(([buffer, blob]) => {
setUploadedFile(
() =>
new File(buffer, "userAudio.mp3", {
type: blob.type,
lastModified: Date.now(),
})
);
});
} else {
setRecording(() => true);
recorder.start();
}
}, [recorder, recording]);
const uploadRecord = useCallback(() => {
setUploadedFile(
() => document.getElementById("icon-button-file").files[0]
);
}, []);
const handleSubmit = useCallback(
async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append("audioFile", uploadedFile, "userAudio.mp3");
const instance = createRequest();
instance.defaults.headers.common["Content-Type"] =
"multipart/form-data";
return instance.post("/auth/audio", formData);
},
[uploadedFile]
);
return (
<RecordMp3Component
togglePlay={togglePlay}
record={record}
uploadRecord={uploadRecord}
handleSubmit={handleSubmit}
/>
);
}

View file

@ -1,94 +0,0 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import Cookies from 'js-cookie';
import MicRecorder from 'mic-recorder-to-mp3';
import { useUser } from '../contexts';
import { RecordMp3 } from '../components';
import { setAudio, getAudio } from '../services/userServices';
function RecordMp3Controller() {
const { userData } = useUser();
const [recorder] = useState(
new MicRecorder({
bitRate: 128,
})
);
const [recording, setRecording] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
const [audioRecording, setAudioRecording] = useState(null);
useEffect(() => {
if (!userData.loading) {
getAudio(userData.id).then((data) => {
setUploadedFile(() => {
// Retrieve audio file of user if exists
return new File([data], 'userAudio.mp3', {
type: data.type,
lastModified: Date.now(),
});
});
});
}
}, [userData]);
function togglePlay() {
if (audioRecording && !audioRecording.ended) {
// Reset audio and reset if track is playing
audioRecording.pause();
audioRecording.currentTime = 0;
setAudioRecording(() => null);
} else {
// Set audio to file and play
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
setAudioRecording(() => audioFile);
audioFile.play();
}
}
function record() {
if (recording) {
// Stop recording then set uploaded file to mp3 instance
setRecording(() => false);
recorder
.stop()
.getMp3()
.then(([buffer, blob]) => {
setUploadedFile(() => {
return new File(buffer, 'userAudio.mp3', {
type: blob.type,
lastModified: Date.now(),
});
});
});
} else {
// Start recording
setRecording(() => true);
recorder.start().catch((e) => {
console.error(e);
});
}
}
function uploadRecord() {
setUploadedFile(() => {
return document.getElementById('icon-button-file').files[0];
});
}
async function handleSubmit(e) {
e.preventDefault();
// Create form data object and attach file and user token
var formData = new FormData();
formData.append('audioFile', uploadedFile, 'userAudio.mp3');
formData.append('token', Cookies.get('token'));
await setAudio(formData);
}
return <RecordMp3 {...{ togglePlay, record, uploadRecord, handleSubmit }} />;
}
export default RecordMp3Controller;

View file

@ -0,0 +1,52 @@
import React, { useCallback, useState } from "react";
import { RecoverForm } from "../components";
import createRequest from "../utils/requests";
import { APIError } from "../types";
function RecoverFormController() {
const [form, setForm] = useState({
email: "",
});
const [error, setError] = useState("");
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
const { value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
},
[]
);
const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const instance = createRequest();
await instance.post("/auth/reset", form);
} catch (e) {
const error = e as APIError;
if (error.response.data.error) {
setError(error.response.data.error);
} else {
setError("An error occurred");
}
}
},
[form]
);
return (
<RecoverForm
form={form}
error={error}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
);
}
export default RecoverFormController;

View file

@ -1,36 +0,0 @@
import React, { useState } from 'react';
import { RecoverForm } from '../components';
import { requestReset } from '../services/userServices';
function RecoverFormController() {
const [form, setForm] = useState({
email: '',
});
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 requestReset(form);
}
return (
<RecoverForm
form={form}
error={error}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
);
}
export default RecoverFormController;

View file

@ -0,0 +1,65 @@
import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { RegisterForm } from "../components";
import { useError } from "../contexts";
import createRequest from "../utils/requests";
import { APIError } from "../types";
function RegisterFormController() {
const navigate = useNavigate();
const [params] = useSearchParams();
const { error, setError } = useError();
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
pronouns: "",
password: "",
confirmPassword: "",
});
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const { name } = e.target;
const { value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
}
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { confirmPassword, ...filteredForm } = form;
const fullForm = { ...filteredForm, orgID: params.get("orgID") };
try {
const instance = createRequest();
await instance.post("/auth/register", fullForm);
navigate("/login");
} catch (e) {
const error = e as APIError;
if (error.response.data.error) {
setError(error.response.data.error);
} else {
setError("An error occurred");
}
}
}
return (
<RegisterForm
{...{
form,
handleSubmit,
handleChange,
error,
}}
/>
);
}
export default RegisterFormController;

View file

@ -1,47 +0,0 @@
import React, { useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import RegisterForm from '../components/RegisterForm';
import { useError } from '../contexts';
import { register } from '../services/userServices';
function RegisterFormController() {
const navigate = useNavigate();
const [params] = useSearchParams();
const { error, setError } = useError();
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
pronouns: '',
password: '',
confirmPassword: '',
});
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;
const { error } = await register({ ...filteredForm, orgID: params.get('orgID') });
if (error) {
setError(() => error);
} else {
navigate('/login');
}
}
return <RegisterForm {...{ form, handleSubmit, handleChange, error }} />;
}
export default RegisterFormController;

View file

@ -0,0 +1,47 @@
import { useCallback, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ResetForm as ResetFormComponent } from "../components";
import { useError } from "../contexts";
import createRequest from "../utils/requests";
export default function ResetForm() {
const navigate = useNavigate();
const { id } = useParams();
const [form, setForm] = useState({
password: "",
confirmPassword: "",
});
const { error } = useError();
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name } = e.target;
const { value } = e.target;
setForm((form) => ({ ...form, [name]: value }));
},
[]
);
const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const instance = createRequest();
await instance.patch(`/reset/${id}`);
navigate("/login");
},
[id, navigate]
);
return (
<ResetFormComponent
form={form}
handleChange={handleChange}
handleSubmit={handleSubmit}
error={error}
/>
);
}

View file

@ -1,49 +0,0 @@
import React, { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ResetForm } from '../components';
import { resetPassword } from '../services/userServices';
import { useError } from '../contexts';
function ResetFormController() {
const navigate = useNavigate();
const { id } = useParams();
const [form, setForm] = useState({
password: '',
confirmPassword: '',
});
const { error, setError } = useError();
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 error = await resetPassword(id, { password: form.password });
if (error) {
setError(() => error);
} else {
navigate('/login');
}
}
return (
<ResetForm
form={form}
handleSubmit={handleSubmit}
handleChange={handleChange}
error={error}
/>
);
}
export default ResetFormController;

View file

@ -0,0 +1,39 @@
import { useCallback, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { UpdateOrganizationForm as UpdateOrganizationFormComponent } from "../components";
import createRequest from "../utils/requests";
export default function UpdateOrganizationForm() {
const { orgID } = useParams();
const navigate = useNavigate();
const [name, setName] = useState("");
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setName(() => value);
},
[]
);
const handleSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const instance = createRequest();
instance.patch(`/org/${orgID}`, { orgName: name });
navigate(`/org/${orgID}`);
},
[name, orgID, navigate]
);
return (
<UpdateOrganizationFormComponent
name={name}
handleSubmit={handleSubmit}
handleChange={handleChange}
/>
);
}

Some files were not shown because too many files have changed in this diff Show more