Added typescript and refactoring
This commit is contained in:
parent
17590cc43d
commit
db1e41b949
158 changed files with 10769 additions and 20499 deletions
|
|
@ -1,2 +0,0 @@
|
||||||
node_modules
|
|
||||||
.env
|
|
||||||
12
.eslintrc.yml
Normal file
12
.eslintrc.yml
Normal 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"
|
||||||
25
client/.eslintrc.yml
Normal file
25
client/.eslintrc.yml
Normal 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
41
client/.gitignore
vendored
|
|
@ -1,23 +1,24 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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?
|
||||||
|
|
|
||||||
13
client/index.html
Normal file
13
client/index.html
Normal 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>
|
||||||
|
|
@ -1,46 +1,47 @@
|
||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.1.0",
|
"private": true,
|
||||||
"private": true,
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"type": "module",
|
||||||
"@emotion/react": "^11.8.1",
|
"scripts": {
|
||||||
"@emotion/styled": "^11.8.1",
|
"dev": "vite",
|
||||||
"@mui/icons-material": "^5.4.4",
|
"build": "tsc && vite build",
|
||||||
"@mui/material": "^5.4.4",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"@mui/x-data-grid": "^5.6.0",
|
"preview": "vite preview"
|
||||||
"axios": "^0.26.1",
|
},
|
||||||
"buffer": "^6.0.3",
|
"dependencies": {
|
||||||
"js-cookie": "^3.0.1",
|
"@emotion/react": "^11.11.3",
|
||||||
"mic-recorder-to-mp3": "^2.2.2",
|
"@emotion/styled": "^11.11.0",
|
||||||
"react": "^17.0.2",
|
"@mui/icons-material": "^5.15.10",
|
||||||
"react-dom": "^17.0.2",
|
"@mui/material": "^5.15.10",
|
||||||
"react-helmet": "^6.1.0",
|
"@mui/x-data-grid": "^6.19.4",
|
||||||
"react-router-dom": "^6.2.2",
|
"axios": "^1.6.7",
|
||||||
"react-scripts": "5.0.0",
|
"buffer": "^6.0.3",
|
||||||
"serve": "^14.2.1"
|
"js-cookie": "^3.0.5",
|
||||||
},
|
"mic-recorder-to-mp3": "^2.2.2",
|
||||||
"scripts": {
|
"prop-types": "^15.8.1",
|
||||||
"start": "react-scripts start",
|
"react": "^18.2.0",
|
||||||
"build": "react-scripts build",
|
"react-dom": "^18.2.0",
|
||||||
"test": "react-scripts test",
|
"react-helmet": "^6.1.0",
|
||||||
"eject": "react-scripts eject"
|
"react-router-dom": "^6.22.1",
|
||||||
},
|
"recordrtc": "^5.6.2"
|
||||||
"eslintConfig": {
|
},
|
||||||
"extends": [
|
"devDependencies": {
|
||||||
"react-app",
|
"@mui/types": "^7.2.13",
|
||||||
"react-app/jest"
|
"@types/js-cookie": "^3.0.6",
|
||||||
]
|
"@types/node": "^20.11.19",
|
||||||
},
|
"@types/react": "^18.2.55",
|
||||||
"browserslist": {
|
"@types/react-dom": "^18.2.19",
|
||||||
"production": [
|
"@types/react-helmet": "^6.1.11",
|
||||||
">0.2%",
|
"@types/recordrtc": "^5.6.14",
|
||||||
"not dead",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"not op_mini all"
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
],
|
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||||
"development": [
|
"eslint": "^8.56.0",
|
||||||
"last 1 chrome version",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"last 1 firefox version",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"last 1 safari version"
|
"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 |
|
|
@ -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>
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
@ -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
56
client/src/App.tsx
Normal 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;
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
|
|
@ -13,14 +13,31 @@ import {
|
||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
} from '@mui/material';
|
Theme,
|
||||||
import { DataGrid } from '@mui/x-data-grid';
|
} from "@mui/material";
|
||||||
import { Link } from 'react-router-dom';
|
import { DataGrid } from "@mui/x-data-grid";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { MemberUnit } from '.';
|
import MemberUnit from "./MemberUnit";
|
||||||
import { DynamicPaper, DynamicStack } from './StyledElements';
|
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,
|
rows,
|
||||||
open,
|
open,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
|
|
@ -33,28 +50,28 @@ function Dashboard({
|
||||||
copyJoinLink,
|
copyJoinLink,
|
||||||
removeSelected,
|
removeSelected,
|
||||||
leaveOrg,
|
leaveOrg,
|
||||||
}) {
|
}: DashboardProps) {
|
||||||
const xs = useMediaQuery((theme) => theme.breakpoints.only('xs'));
|
const xs = useMediaQuery((theme: Theme) => theme.breakpoints.only("xs"));
|
||||||
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
|
const sm = useMediaQuery((theme: Theme) => theme.breakpoints.only("sm"));
|
||||||
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
|
const md = useMediaQuery((theme: Theme) => theme.breakpoints.only("md"));
|
||||||
|
|
||||||
const linkStyle = { textDecoration: 'none', color: 'inherit' };
|
const linkStyle = { textDecoration: "none", color: "inherit" };
|
||||||
const columns = [
|
const columns = [
|
||||||
{ field: 'id', headerName: 'ID', flex: 1 },
|
{ field: "id", headerName: "ID", flex: 1 },
|
||||||
{ field: 'firstName', headerName: 'First Name', flex: 3 },
|
{ field: "firstName", headerName: "First Name", flex: 3 },
|
||||||
{ field: 'lastName', headerName: 'Last Name', flex: 3 },
|
{ field: "lastName", headerName: "Last Name", flex: 3 },
|
||||||
{ field: 'nickname', headerName: 'Nickname', flex: 2 },
|
{ field: "nickname", headerName: "Nickname", flex: 2 },
|
||||||
{
|
{
|
||||||
field: 'namePronounciation',
|
field: "namePronounciation",
|
||||||
headerName: 'Pronounciation',
|
headerName: "Pronounciation",
|
||||||
flex: 2,
|
flex: 2,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
renderCell: (params) => (
|
renderCell: (params: User) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
size='large'
|
size="large"
|
||||||
edge='start'
|
edge="start"
|
||||||
color='inherit'
|
color="inherit"
|
||||||
aria-label='menu'
|
aria-label="menu"
|
||||||
sx={{ mr: 2 }}
|
sx={{ mr: 2 }}
|
||||||
onClick={() => onClick(params.id)}
|
onClick={() => onClick(params.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -62,53 +79,63 @@ function Dashboard({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ field: 'pronouns', headerName: 'Pronouns', flex: 2 },
|
{ field: "pronouns", headerName: "Pronouns", flex: 2 },
|
||||||
{ field: 'email', headerName: 'Email', flex: 10, sortable: false },
|
{
|
||||||
|
field: "email",
|
||||||
|
headerName: "Email",
|
||||||
|
flex: 10,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const buttonGroupOrientation = () => {
|
const buttonGroupOrientation = () => (xs ? "vertical" : "horizontal");
|
||||||
if (xs) return 'vertical';
|
|
||||||
};
|
|
||||||
|
|
||||||
function calculateCollapsedSize() {
|
function calculateCollapsedSize() {
|
||||||
if (xs) return '37vh';
|
if (xs) return "37vh";
|
||||||
if (sm) return '63vh';
|
if (sm) return "63vh";
|
||||||
if (md) return '68vh';
|
return "68vh";
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateTableHeight() {
|
function calculateTableHeight() {
|
||||||
return open ? '64vh' : '100%';
|
return open ? "64vh" : "100%";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box sx={{ padding: '2vh' }}>
|
<Box sx={{ padding: "2vh" }}>
|
||||||
<Accordion expanded={open} onChange={handleOpen}>
|
<Accordion expanded={open} onChange={handleOpen}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography variant='h6'>Organizations / {org.name}</Typography>
|
<Typography variant="h6">
|
||||||
|
Organizations / {org.name}
|
||||||
|
</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails>
|
||||||
<Box sx={{ marginTop: '1vh' }} fullWidth>
|
<Box>
|
||||||
<Grid container columns={{ xs: 3, sm: 12, md: 12 }} spacing={1}>
|
<Grid
|
||||||
|
container
|
||||||
|
columns={{ xs: 3, sm: 12, md: 12 }}
|
||||||
|
spacing={1}
|
||||||
|
>
|
||||||
<Grid item xs={3}>
|
<Grid item xs={3}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Typography variant='body1'>
|
<Typography variant="body1">
|
||||||
Organization ID: {org.id}
|
Organization ID: {org.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>
|
<Typography variant="body1">
|
||||||
Members: {org.memberCount}
|
Members: {org.memberCount}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>
|
<Typography variant="body1">
|
||||||
Created On: {org.createdAt}
|
Created On: {org.createdAt}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={3}>
|
<Grid item xs={3}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Typography variant='body1'>
|
<Typography variant="body1">
|
||||||
Owner: {`${org.owner.firstName} ${org.owner.lastName}`}
|
Owner:{" "}
|
||||||
|
{`${org.owner.firstName} ${org.owner.lastName}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1'>
|
<Typography variant="body1">
|
||||||
Email: {org.owner.email}
|
Email: {org.owner.email}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
@ -118,35 +145,53 @@ function Dashboard({
|
||||||
{status ? (
|
{status ? (
|
||||||
<>
|
<>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
variant='outlined'
|
variant="outlined"
|
||||||
orientation={buttonGroupOrientation()}
|
orientation={buttonGroupOrientation()}
|
||||||
>
|
>
|
||||||
<Button color='warning'>
|
<Button color="warning">
|
||||||
<Link to='update' style={linkStyle}>
|
<Link
|
||||||
|
to="update"
|
||||||
|
style={linkStyle}
|
||||||
|
>
|
||||||
Edit {org.name}
|
Edit {org.name}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button color='error' onClick={handleDelete}>
|
<Button
|
||||||
|
color="error"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
Delete {org.name}
|
Delete {org.name}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
variant='outlined'
|
variant="outlined"
|
||||||
orientation={buttonGroupOrientation()}
|
orientation={buttonGroupOrientation()}
|
||||||
>
|
>
|
||||||
<Button color='success' onClick={copyID}>
|
<Button
|
||||||
|
color="success"
|
||||||
|
onClick={copyID}
|
||||||
|
>
|
||||||
Copy Org ID
|
Copy Org ID
|
||||||
</Button>
|
</Button>
|
||||||
<Button color='success' onClick={copyJoinLink}>
|
<Button
|
||||||
|
color="success"
|
||||||
|
onClick={copyJoinLink}
|
||||||
|
>
|
||||||
Copy Org Join Link
|
Copy Org Join Link
|
||||||
</Button>
|
</Button>
|
||||||
<Button color='error' onClick={removeSelected}>
|
<Button
|
||||||
|
color="error"
|
||||||
|
onClick={removeSelected}
|
||||||
|
>
|
||||||
Remove People
|
Remove People
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button color='error' onClick={leaveOrg}>
|
<Button
|
||||||
|
color="error"
|
||||||
|
onClick={leaveOrg}
|
||||||
|
>
|
||||||
Leave Org
|
Leave Org
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -157,12 +202,12 @@ function Dashboard({
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</Box>
|
</Box>
|
||||||
<Collapse collapsedSize={calculateCollapsedSize()} in={open ? false : true}>
|
<Collapse collapsedSize={calculateCollapsedSize()} in={!open}>
|
||||||
<DynamicPaper>
|
<DynamicPaper>
|
||||||
{(xs || sm) &&
|
{(xs || sm) &&
|
||||||
rows.map((member) => {
|
rows.map((member) => (
|
||||||
return <MemberUnit member={member} onClick={onClick} />;
|
<MemberUnit member={member} onClick={onClick} />
|
||||||
})}
|
))}
|
||||||
{md && (
|
{md && (
|
||||||
<Box height={calculateTableHeight()}>
|
<Box height={calculateTableHeight()}>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
|
|
@ -182,5 +227,3 @@ function Dashboard({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
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;
|
|
||||||
80
client/src/components/EditUserForm.tsx
Normal file
80
client/src/components/EditUserForm.tsx
Normal 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;
|
||||||
|
|
@ -1,35 +1,51 @@
|
||||||
import { Alert, Box, Card, CardContent, Grid, Typography } from '@mui/material';
|
import {
|
||||||
import { useTheme } from '@mui/material/styles';
|
Alert, Box, Card, CardContent, Grid, Typography,
|
||||||
import { Outlet } from 'react-router-dom';
|
} 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();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<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={0.5} sm={2} md={4} />
|
||||||
<Grid item xs={11} sm={8} md={4}>
|
<Grid item xs={11} sm={8} md={4}>
|
||||||
<Card
|
<Card
|
||||||
sx={{
|
sx={{
|
||||||
...theme.typography.body2,
|
...theme.typography.body2,
|
||||||
padding: '2vh',
|
padding: "2vh",
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{text && (
|
{text && (
|
||||||
<Typography
|
<Typography
|
||||||
variant='h5'
|
variant="h5"
|
||||||
sx={{ margin: '1vh 0vh', fontWeight: 'bold' }}
|
sx={{
|
||||||
|
margin: "1vh 0vh",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
<Outlet />
|
<Outlet />
|
||||||
{error && <Alert severity='error'>{error}</Alert>}
|
{error && <Alert severity="error">{error}</Alert>}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { Checkbox, FormControlLabel, Typography } from '@mui/material';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
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;
|
|
||||||
77
client/src/components/LoginForm.tsx
Normal file
77
client/src/components/LoginForm.tsx
Normal 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;
|
||||||
|
|
@ -1,57 +1,88 @@
|
||||||
import { Box, Grid, Stack, Typography } from '@mui/material';
|
import {
|
||||||
import { Link } from 'react-router-dom';
|
Box, Grid, Stack, Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import OrgUnit from './OrgUnit';
|
import OrgUnit from "./OrgUnit";
|
||||||
import StackItem from './StackItem';
|
import StackItem from "./StackItem";
|
||||||
import { Form, FormButton, FormField } from './StyledElements';
|
import { Form, FormButton, FormField } from "./StyledElements";
|
||||||
|
|
||||||
function MainPageBase({ form, orgs, ownedOrgs, joinOrg, createOrg, handleChange }) {
|
import { Organization } from "../types";
|
||||||
const linkStyle = { textDecoration: 'none', color: 'inherit' };
|
|
||||||
|
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 (
|
return (
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
<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={0.5} sm={2} md={4} />
|
||||||
<Grid item xs={11} sm={8} md={4}>
|
<Grid item xs={11} sm={8} md={4}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<StackItem text='Join an Organization'>
|
<StackItem text="Join an Organization">
|
||||||
<form onSubmit={joinOrg}>
|
<form onSubmit={joinOrg}>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField
|
<FormField
|
||||||
label='Organization ID'
|
label="Organization ID"
|
||||||
name='id'
|
name="id"
|
||||||
value={form.id}
|
value={form.id}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<FormButton type='submit' variant='outlined'>
|
<FormButton
|
||||||
|
type="submit"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
Join Organization
|
Join Organization
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Form>
|
</Form>
|
||||||
</form>
|
</form>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem text='Create an Organization'>
|
<StackItem text="Create an Organization">
|
||||||
<form onSubmit={createOrg}>
|
<form onSubmit={createOrg}>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField
|
<FormField
|
||||||
label='Organization Name'
|
label="Organization Name"
|
||||||
name='name'
|
name="name"
|
||||||
value={form.name}
|
value={form.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<FormButton type='submit' variant='outlined'>
|
<FormButton
|
||||||
|
type="submit"
|
||||||
|
variant="outlined"
|
||||||
|
>
|
||||||
Create Organization
|
Create Organization
|
||||||
</FormButton>
|
</FormButton>
|
||||||
</Form>
|
</Form>
|
||||||
</form>
|
</form>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Typography variant='h6'>
|
<Typography variant="h6">
|
||||||
<Link to='org' style={linkStyle}>
|
<Link to="org" style={linkStyle}>
|
||||||
Your Organizations
|
Your Organizations
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
{ownedOrgs.length === 0 && (
|
{ownedOrgs.length === 0 && (
|
||||||
<Typography variant='body2'>
|
<Typography variant="body2">
|
||||||
You do own any organizations
|
You do own any organizations
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
@ -61,13 +92,13 @@ function MainPageBase({ form, orgs, ownedOrgs, joinOrg, createOrg, handleChange
|
||||||
))}
|
))}
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Typography variant='h6'>
|
<Typography variant="h6">
|
||||||
<Link to='org/joined' style={linkStyle}>
|
<Link to="org/joined" style={linkStyle}>
|
||||||
Joined Organizations
|
Joined Organizations
|
||||||
</Link>
|
</Link>
|
||||||
</Typography>
|
</Typography>
|
||||||
{orgs.length === 0 && (
|
{orgs.length === 0 && (
|
||||||
<Typography variant='body2'>
|
<Typography variant="body2">
|
||||||
You are not a member in any organizations
|
You are not a member in any organizations
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
@ -83,5 +114,3 @@ function MainPageBase({ form, orgs, ownedOrgs, joinOrg, createOrg, handleChange
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MainPageBase;
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Grid,
|
|
||||||
IconButton,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
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;
|
|
||||||
57
client/src/components/MemberUnit.tsx
Normal file
57
client/src/components/MemberUnit.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,89 +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 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
125
client/src/components/MobileNavbar.tsx
Normal file
125
client/src/components/MobileNavbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
88
client/src/components/Navbar.tsx
Normal file
88
client/src/components/Navbar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|
||||||
import { Accordion, AccordionDetails, AccordionSummary, Typography } from '@mui/material';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
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;
|
|
||||||
44
client/src/components/OrgUnit.tsx
Normal file
44
client/src/components/OrgUnit.tsx
Normal 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;
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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;
|
|
||||||
35
client/src/components/OrganizationList.tsx
Normal file
35
client/src/components/OrganizationList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
47
client/src/components/RecordMp3.tsx
Normal file
47
client/src/components/RecordMp3.tsx
Normal 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;
|
||||||
|
|
@ -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;
|
|
||||||
37
client/src/components/RecoverForm.tsx
Normal file
37
client/src/components/RecoverForm.tsx
Normal 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;
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
import { Typography } from '@mui/material';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
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;
|
|
||||||
90
client/src/components/RegisterForm.tsx
Normal file
90
client/src/components/RegisterForm.tsx
Normal 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;
|
||||||
|
|
@ -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;
|
|
||||||
51
client/src/components/ResetForm.tsx
Normal file
51
client/src/components/ResetForm.tsx
Normal 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;
|
||||||
|
|
@ -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;
|
|
||||||
26
client/src/components/StackItem.tsx
Normal file
26
client/src/components/StackItem.tsx
Normal 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;
|
||||||
|
|
@ -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 { DynamicPaper, DynamicStack, Form, FormButton, FormField, FormSubmit, FormTextField };
|
|
||||||
119
client/src/components/StyledElements.tsx
Normal file
119
client/src/components/StyledElements.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -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;
|
|
||||||
31
client/src/components/UpdateOrganizationForm.tsx
Normal file
31
client/src/components/UpdateOrganizationForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import Dashboard from './Dashboard';
|
|
||||||
import EditUserForm from './EditUserForm';
|
|
||||||
import Form from './Form';
|
|
||||||
import LoginForm from './LoginForm';
|
|
||||||
import MainPageBase from './MainPageBase';
|
|
||||||
import MemberUnit from './MemberUnit';
|
|
||||||
import MobileNavbar from './MobileNavbar';
|
|
||||||
import Navbar from './Navbar';
|
|
||||||
import OrgUnit from './OrgUnit';
|
|
||||||
import OrganizationList from './OrganizationList';
|
|
||||||
import RecordMp3 from './RecordMp3';
|
|
||||||
import RecoverForm from './RecoverForm';
|
|
||||||
import ResetForm from './ResetForm';
|
|
||||||
import StackItem from './StackItem';
|
|
||||||
import UpdateOrganizationForm from './UpdateOrganizationForm';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dashboard, EditUserForm,
|
|
||||||
Form, LoginForm,
|
|
||||||
MainPageBase, MemberUnit, MobileNavbar, Navbar, OrgUnit, OrganizationList, RecordMp3,
|
|
||||||
RecoverForm,
|
|
||||||
ResetForm, StackItem, UpdateOrganizationForm
|
|
||||||
};
|
|
||||||
27
client/src/components/index.ts
Normal file
27
client/src/components/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from "react-helmet";
|
||||||
|
|
||||||
import { EditUserForm } from '../controllers';
|
import { EditUserForm } from "../controllers";
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
import { Box, CssBaseline } from '@mui/material';
|
import { Box, CssBaseline } from "@mui/material";
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
import { ThemeProvider, createTheme } from "@mui/material/styles";
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from "react-helmet";
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
import { useMode } from '../contexts';
|
import { useMode } from "../contexts";
|
||||||
import { Navbar } from '../controllers';
|
import { Navbar } from "../controllers";
|
||||||
|
|
||||||
|
declare module "@mui/material/styles" {
|
||||||
|
interface BreakpointOverrides {
|
||||||
|
xs: true;
|
||||||
|
sm: true;
|
||||||
|
md: true;
|
||||||
|
lg: false;
|
||||||
|
xl: false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Layout() {
|
function Layout() {
|
||||||
const { mode } = useMode();
|
const { mode } = useMode();
|
||||||
|
|
@ -28,7 +38,7 @@ function Layout() {
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<main>
|
<main>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Box sx={{ height: { xs: '', sm: '92vh', md: '92vh' } }}>
|
<Box sx={{ height: { xs: "", sm: "92vh", md: "92vh" } }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Box>
|
</Box>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from "react-helmet";
|
||||||
|
|
||||||
import { LoginForm } from '../controllers';
|
import { LoginForm } from "../controllers";
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import Helmet from 'react-helmet';
|
|
||||||
|
|
||||||
import { MainPageDisplay } from '../controllers';
|
|
||||||
|
|
||||||
function MainPage() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>KnowItAll</title>
|
|
||||||
</Helmet>
|
|
||||||
<MainPageDisplay />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MainPage;
|
|
||||||
14
client/src/containers/MainPage.tsx
Normal file
14
client/src/containers/MainPage.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
@ -12,5 +12,3 @@ function MemberOrgs() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MemberOrgs;
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
@ -12,5 +12,3 @@ function OrgDashboard() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrgDashboard;
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from "react-helmet";
|
||||||
|
|
||||||
import { OwnedOrgsDashboard } from '../controllers';
|
import { OwnedOrgsDashboard } from "../controllers";
|
||||||
|
|
||||||
function OwnedOrgs() {
|
function OwnedOrgs() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from "react-helmet";
|
||||||
|
|
||||||
import { RecoverForm } from '../controllers';
|
import { RecoverForm } from "../controllers";
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from "react-helmet";
|
||||||
|
|
||||||
import { RegisterForm } from '../controllers';
|
import { RegisterForm } from "../controllers";
|
||||||
|
|
||||||
function Register() {
|
function Register() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
@ -12,5 +12,3 @@ function Reset() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Reset;
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
@ -12,5 +12,3 @@ function UpdateOrganization() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UpdateOrganization;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import EditUser from './EditUser';
|
|
||||||
import Layout from './Layout';
|
|
||||||
import Login from './Login';
|
|
||||||
import MainPage from './MainPage';
|
|
||||||
import MemberOrgs from './MemberOrgs';
|
|
||||||
import OrgDashboard from './OrgDashboard';
|
|
||||||
import OwnedOrgs from './OwnedOrgs';
|
|
||||||
import Recover from './Recover';
|
|
||||||
import Register from './Register';
|
|
||||||
import Reset from './Reset';
|
|
||||||
import UpdateOrganization from './UpdateOrganization';
|
|
||||||
|
|
||||||
export {
|
|
||||||
EditUser, Layout,
|
|
||||||
Login, MainPage, MemberOrgs,
|
|
||||||
OrgDashboard, OwnedOrgs, Recover, Register, Reset, UpdateOrganization
|
|
||||||
};
|
|
||||||
25
client/src/containers/index.ts
Normal file
25
client/src/containers/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { ErrorProvider, ModeProvider, UserProvider } from '.';
|
|
||||||
|
|
||||||
export default function ContextProvider({ children }) {
|
|
||||||
return (
|
|
||||||
<ModeProvider>
|
|
||||||
<ErrorProvider>
|
|
||||||
<UserProvider>{children}</UserProvider>
|
|
||||||
</ErrorProvider>
|
|
||||||
</ModeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
19
client/src/contexts/ContextProvider.tsx
Normal file
19
client/src/contexts/ContextProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
20
client/src/contexts/ErrorContext.ts
Normal file
20
client/src/contexts/ErrorContext.ts
Normal 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;
|
||||||
|
};
|
||||||
22
client/src/contexts/ErrorProvider.tsx
Normal file
22
client/src/contexts/ErrorProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
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);
|
|
||||||
22
client/src/contexts/ModeContext.ts
Normal file
22
client/src/contexts/ModeContext.ts
Normal 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;
|
||||||
|
};
|
||||||
27
client/src/contexts/ModeProvider.tsx
Normal file
27
client/src/contexts/ModeProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
const UserContext = createContext();
|
|
||||||
|
|
||||||
export default function UserProvider({ children }) {
|
|
||||||
const [userData, setUserData] = useState({});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const loadUserData = useCallback(async () => {
|
|
||||||
if (!loading) return;
|
|
||||||
|
|
||||||
if (!Cookies.get('token')) {
|
|
||||||
setLoading(() => false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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={{
|
|
||||||
loading,
|
|
||||||
userData,
|
|
||||||
setUserData,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</UserContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUser = () => useContext(UserContext);
|
|
||||||
23
client/src/contexts/UserContext.ts
Normal file
23
client/src/contexts/UserContext.ts
Normal 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;
|
||||||
|
};
|
||||||
63
client/src/contexts/UserProvider.tsx
Normal file
63
client/src/contexts/UserProvider.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
|
||||||
8
client/src/contexts/index.ts
Normal file
8
client/src/contexts/index.ts
Normal 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,
|
||||||
|
};
|
||||||
113
client/src/controllers/Dashboard.tsx
Normal file
113
client/src/controllers/Dashboard.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { Dashboard } from '../components';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
function handleOpen() {
|
|
||||||
setOpen((initial) => (initial ? false : true));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete() {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.delete(`/org/${orgID}`);
|
|
||||||
navigate('/');
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.post(`/org/${orgID}/remove`, { doomedUserIDs });
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leaveOrg() {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.post(`/org/${orgID}/remove`, {});
|
|
||||||
navigate('/');
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function play(id) {
|
|
||||||
try {
|
|
||||||
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();
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
67
client/src/controllers/EditUserForm.tsx
Normal file
67
client/src/controllers/EditUserForm.tsx
Normal 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;
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { EditUserForm } from '../components';
|
|
||||||
import { useUser } 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, setError] = useState('');
|
|
||||||
|
|
||||||
function handleChange(e) {
|
|
||||||
const name = e.target.name;
|
|
||||||
const value = e.target.value;
|
|
||||||
|
|
||||||
setForm((form) => ({ ...form, [name]: value }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.post('/auth/update', form);
|
|
||||||
navigate('/');
|
|
||||||
} catch (error) {
|
|
||||||
setError(() => error.response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
89
client/src/controllers/LoginForm.tsx
Normal file
89
client/src/controllers/LoginForm.tsx
Normal 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;
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import { Buffer } from 'buffer';
|
|
||||||
import Cookies from 'js-cookie';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { LoginForm } from '../components';
|
|
||||||
import { useError, useUser } from '../contexts';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
function LoginFormController({ setToken }) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { error, setError } = useError();
|
|
||||||
const { setUserData } = useUser();
|
|
||||||
|
|
||||||
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 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 (error) {
|
|
||||||
setError(() => error.response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <LoginForm {...{ form, handleSubmit, handleChange, handleCheckboxChange, error }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LoginFormController;
|
|
||||||
70
client/src/controllers/MainPage.tsx
Normal file
70
client/src/controllers/MainPage.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { MainPageBase } from '../components';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
function MainPageController() {
|
|
||||||
const [ownedOrgs, setOwnedOrgs] = useState([]);
|
|
||||||
const [orgs, setOrgs] = useState([]);
|
|
||||||
const [form, setForm] = useState({
|
|
||||||
name: '',
|
|
||||||
id: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOwnedOrgs = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
const response = await instance.get('/auth/orgs');
|
|
||||||
setOwnedOrgs(() => response.data.orgs);
|
|
||||||
} catch (error) {}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getMemberOrgs = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
const response = await instance.get('/auth/orgs/member');
|
|
||||||
setOrgs(() => response.data.orgs);
|
|
||||||
} catch (error) {}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getOwnedOrgs();
|
|
||||||
getMemberOrgs();
|
|
||||||
}, [getOwnedOrgs, getMemberOrgs]);
|
|
||||||
|
|
||||||
function handleChange(e) {
|
|
||||||
const name = e.target.name;
|
|
||||||
const value = e.target.value;
|
|
||||||
|
|
||||||
setForm((form) => {
|
|
||||||
return { ...form, [name]: value };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function makeOrg(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const instance = createRequest();
|
|
||||||
instance.post('/org/create', form).then(getOwnedOrgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function joinOrg(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const instance = createRequest();
|
|
||||||
instance.post(`/org/${form.id}/add`, {}).then(getMemberOrgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MainPageBase
|
|
||||||
form={form}
|
|
||||||
orgs={orgs}
|
|
||||||
ownedOrgs={ownedOrgs}
|
|
||||||
handleChange={handleChange}
|
|
||||||
joinOrg={joinOrg}
|
|
||||||
createOrg={makeOrg}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MainPageController;
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { OrganizationList } from '../components';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
function MemberOrgsDashboard() {
|
|
||||||
const [orgs, setOrgs] = useState([]);
|
|
||||||
|
|
||||||
const getOrgs = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
const response = await instance.get('/auth/orgs');
|
|
||||||
setOrgs(() => response.data.orgs);
|
|
||||||
} catch (error) {}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(getOrgs, [getOrgs]);
|
|
||||||
|
|
||||||
return <OrganizationList orgs={orgs} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MemberOrgsDashboard;
|
|
||||||
21
client/src/controllers/MemberOrgsDashboard.tsx
Normal file
21
client/src/controllers/MemberOrgsDashboard.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
61
client/src/controllers/Navbar.tsx
Normal file
61
client/src/controllers/Navbar.tsx
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,48 +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 { 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.breakpoints.only('sm'));
|
|
||||||
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
|
|
||||||
|
|
||||||
const logoutUser = async () => {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.post('/auth/logout');
|
|
||||||
} catch (error) {
|
|
||||||
} finally {
|
|
||||||
Cookies.remove('token');
|
|
||||||
setUserData({});
|
|
||||||
navigate('/login');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return md || sm
|
|
||||||
? Navbar({ logout: logoutUser, userData, mode, toggleMode })
|
|
||||||
: MobileNavbar({
|
|
||||||
logout: logoutUser,
|
|
||||||
userData,
|
|
||||||
open,
|
|
||||||
setOpen: toggleOpen,
|
|
||||||
mode,
|
|
||||||
toggleMode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { OrganizationList } from '../components';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
function OwnedOrgsDashboard() {
|
|
||||||
const [orgs, setOrgs] = useState([]);
|
|
||||||
|
|
||||||
const getOrgs = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
const response = await instance.get('/auth/orgs/member');
|
|
||||||
setOrgs(() => response.data.orgs);
|
|
||||||
} catch (error) {}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(getOrgs, [getOrgs]);
|
|
||||||
|
|
||||||
return <OrganizationList orgs={orgs} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OwnedOrgsDashboard;
|
|
||||||
21
client/src/controllers/OwnedOrgsDashboard.tsx
Normal file
21
client/src/controllers/OwnedOrgsDashboard.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
103
client/src/controllers/RecordMp3.jsx
Normal file
103
client/src/controllers/RecordMp3.jsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import axios from 'axios';
|
|
||||||
import MicRecorder from 'mic-recorder-to-mp3';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { RecordMp3 } from '../components';
|
|
||||||
import { useUser } from '../contexts';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const getData = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${userData.id}.mp3`,
|
|
||||||
{ responseType: 'blob' }
|
|
||||||
);
|
|
||||||
setUploadedFile(() => {
|
|
||||||
return new File([response.data], 'userAudio.mp3', {
|
|
||||||
type: response.data.type,
|
|
||||||
lastModified: Date.now(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {}
|
|
||||||
}, [userData.id]);
|
|
||||||
|
|
||||||
useEffect(getData, [getData]);
|
|
||||||
|
|
||||||
function togglePlay() {
|
|
||||||
if (audioRecording && !audioRecording.ended) {
|
|
||||||
audioRecording.pause();
|
|
||||||
audioRecording.currentTime = 0;
|
|
||||||
setAudioRecording(() => null);
|
|
||||||
} else {
|
|
||||||
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
|
|
||||||
setAudioRecording(() => audioFile);
|
|
||||||
audioFile.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function record() {
|
|
||||||
if (recording) {
|
|
||||||
setRecording(() => false);
|
|
||||||
recorder
|
|
||||||
.stop()
|
|
||||||
.getMp3()
|
|
||||||
.then(([buffer, blob]) => {
|
|
||||||
setUploadedFile(() => {
|
|
||||||
return new File(buffer, 'userAudio.mp3', {
|
|
||||||
type: blob.type,
|
|
||||||
lastModified: Date.now(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setRecording(() => true);
|
|
||||||
recorder.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function uploadRecord() {
|
|
||||||
setUploadedFile(() => {
|
|
||||||
return document.getElementById('icon-button-file').files[0];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
var 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <RecordMp3 {...{ togglePlay, record, uploadRecord, handleSubmit }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecordMp3Controller;
|
|
||||||
52
client/src/controllers/RecoverForm.tsx
Normal file
52
client/src/controllers/RecoverForm.tsx
Normal 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;
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import { RecoverForm } from '../components';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
function RecoverFormController() {
|
|
||||||
const [form, setForm] = useState({});
|
|
||||||
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();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.post('/auth/reset', form);
|
|
||||||
} catch (error) {
|
|
||||||
setError(() => error.response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RecoverForm
|
|
||||||
form={form}
|
|
||||||
error={error}
|
|
||||||
handleChange={handleChange}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecoverFormController;
|
|
||||||
65
client/src/controllers/RegisterForm.tsx
Normal file
65
client/src/controllers/RegisterForm.tsx
Normal 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;
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import RegisterForm from '../components/RegisterForm';
|
|
||||||
import { useError } from '../contexts';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
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 fullForm = { ...filteredForm, orgID: params.get('orgID') };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.post('/auth/register', fullForm);
|
|
||||||
navigate('/login');
|
|
||||||
} catch (error) {
|
|
||||||
setError(() => error.response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <RegisterForm {...{ form, handleSubmit, handleChange, error }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RegisterFormController;
|
|
||||||
47
client/src/controllers/ResetForm.tsx
Normal file
47
client/src/controllers/ResetForm.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { ResetForm } from '../components';
|
|
||||||
import { useError } from '../contexts';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.patch(`/reset/${id}`);
|
|
||||||
navigate('/login');
|
|
||||||
} catch (error) {
|
|
||||||
setError(() => error.response.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResetForm
|
|
||||||
form={form}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
handleChange={handleChange}
|
|
||||||
error={error}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ResetFormController;
|
|
||||||
39
client/src/controllers/UpdateOrganizationForm.tsx
Normal file
39
client/src/controllers/UpdateOrganizationForm.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { UpdateOrganizationForm } from '../components';
|
|
||||||
import { createRequest } from '../utils/requests';
|
|
||||||
|
|
||||||
function UpdateOrganizationFormController() {
|
|
||||||
const { orgID } = useParams();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
|
|
||||||
function handleChange(e) {
|
|
||||||
const value = e.target.value;
|
|
||||||
|
|
||||||
setName(() => value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const instance = createRequest();
|
|
||||||
await instance.patch(`/org/${orgID}`, { orgName: name });
|
|
||||||
navigate(`/org/${orgID}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UpdateOrganizationForm
|
|
||||||
name={name}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
handleChange={handleChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UpdateOrganizationFormController;
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import Navbar from './NavbarController';
|
|
||||||
import LoginForm from './LoginFormController';
|
|
||||||
import RegisterForm from './RegisterFormController';
|
|
||||||
import MainPageDisplay from './MainPageController';
|
|
||||||
import EditUserForm from './EditUserFormController';
|
|
||||||
import RecordMp3 from './RecordMp3Controller';
|
|
||||||
import RecoverForm from './RecoverFormController';
|
|
||||||
import ResetForm from './ResetFormController';
|
|
||||||
import OwnedOrgsDashboard from './OwnedOrgsDashboard';
|
|
||||||
import MemberOrgsDashboard from './MemberOrgsDashboard';
|
|
||||||
import Dashboard from './DashboardController';
|
|
||||||
import UpdateOrganizationForm from './UpdateOrganizationFormController';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Navbar,
|
|
||||||
LoginForm,
|
|
||||||
RegisterForm,
|
|
||||||
MainPageDisplay,
|
|
||||||
EditUserForm,
|
|
||||||
RecordMp3,
|
|
||||||
RecoverForm,
|
|
||||||
ResetForm,
|
|
||||||
OwnedOrgsDashboard,
|
|
||||||
MemberOrgsDashboard,
|
|
||||||
Dashboard,
|
|
||||||
UpdateOrganizationForm,
|
|
||||||
};
|
|
||||||
25
client/src/controllers/index.ts
Normal file
25
client/src/controllers/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import Navbar from "./Navbar";
|
||||||
|
import MainPage from "./MainPage";
|
||||||
|
import LoginForm from "./LoginForm";
|
||||||
|
import RegisterForm from "./RegisterForm";
|
||||||
|
import EditUserForm from "./EditUserForm";
|
||||||
|
import RecoverForm from "./RecoverForm";
|
||||||
|
import ResetForm from "./ResetForm";
|
||||||
|
import OwnedOrgsDashboard from "./OwnedOrgsDashboard";
|
||||||
|
import Dashboard from "./Dashboard";
|
||||||
|
import UpdateOrganizationForm from "./UpdateOrganizationForm";
|
||||||
|
import MemberOrgsDashboard from "./MemberOrgsDashboard";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Navbar,
|
||||||
|
MainPage,
|
||||||
|
LoginForm,
|
||||||
|
RegisterForm,
|
||||||
|
EditUserForm,
|
||||||
|
RecoverForm,
|
||||||
|
ResetForm,
|
||||||
|
OwnedOrgsDashboard,
|
||||||
|
Dashboard,
|
||||||
|
UpdateOrganizationForm,
|
||||||
|
MemberOrgsDashboard,
|
||||||
|
};
|
||||||
8
client/src/environment.d.ts
vendored
Normal file
8
client/src/environment.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
declare global {
|
||||||
|
namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
REACT_APP_DOMAIN_ROOT: string;
|
||||||
|
REACT_APP_API_ROOT: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
9
client/src/main.jsx
Normal file
9
client/src/main.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue