Compare commits

...

51 commits

Author SHA1 Message Date
df862da6b2 Remove release-drafter.yml and build-deploy.yml 2024-02-21 22:46:48 -06:00
db1e41b949 Added typescript and refactoring 2024-02-21 22:46:42 -06:00
17590cc43d Update build-deploy.yml, Dockerfile, Dockerfile.dev, LoginFormController.jsx, docker-compose.prod.yml, and docker-compose.yml 2024-02-19 09:54:41 -06:00
1f9a3acc17 Add environment variable to build process 2024-02-19 08:51:25 -06:00
7ee60242d3 Add environment variables for client image build 2024-02-19 08:08:42 -06:00
5d2714e1ee Update Dockerfile and docker-compose files 2024-02-19 06:12:12 -06:00
720e4f37b0 Update Docker configuration for multi-platform support 2024-02-18 21:25:29 -06:00
c099f016f9 Add Dockerfiles for server and client, update package.json scripts 2024-02-18 21:12:50 -06:00
469204a30f Created CI/CD workflow 2024-02-17 22:28:10 -06:00
fb43f05d8a Completely refactored basically everything 2024-02-17 13:23:53 -06:00
ff255baf15 Updated workflows to work with new branch schemas 2022-12-18 22:06:12 -06:00
bc42b39b0c Updated server workflows 2022-12-18 22:01:07 -06:00
a75305be1c Updated build workflows 2022-12-18 21:59:07 -06:00
2dddd857c1 Renamed errors 2022-12-17 16:37:51 -06:00
dc5b510365 Updated org test suite 2022-12-17 16:31:59 -06:00
d8becd3af4 Updated auth test suite 2022-12-17 15:40:31 -06:00
f6c739bfe1 Brought app up to modern standards 2022-12-17 12:12:00 -06:00
9e8e074181 Installed buffer in client 2022-12-15 20:47:47 -06:00
7ca9067533 Updated server to more fit frontend requirements 2022-12-15 20:47:35 -06:00
f5ad8e5f8d Added services 2022-12-15 18:27:00 -06:00
c57c2b35c5 Added user to response 2022-12-15 17:55:30 -06:00
52b76a760c Brought API responses up to standards and added API documentation 2022-12-15 17:39:51 -06:00
6d7d0fc216 Part 1 of code refactor 2022-12-15 13:42:44 -06:00
586567690d Updated server 2022-12-15 13:42:30 -06:00
8f63aea8b6 Updated devscripts 2022-12-13 22:49:13 -06:00
7a0ae20539 Added docker capabilities 2022-12-13 22:49:06 -06:00
b7b3b910ff Removed .vscode folder (extension specific) 2022-12-13 22:48:40 -06:00
73be3993a0 Updated server code to add better error messages and responses 2022-12-13 22:48:16 -06:00
c631e4e6b5 Fixed forms to import from correct location 2022-04-13 21:57:33 -05:00
9d1338c6c0 Added dotenv back into deplist 2022-04-13 21:36:33 -05:00
266892bb26 Merge branch 'development' into separate-controllers 2022-04-13 21:20:32 -05:00
c0a6be7c63 Reordered package scripts 2022-04-13 21:15:49 -05:00
7437610b93 Fixed imports for form route utility elements 2022-04-13 19:16:16 -05:00
b62d177654 Refactored all components into controllers and dummies 2022-04-13 19:14:15 -05:00
62a0cc96cc Added comments and documentation to codebase 2022-04-09 16:17:12 -05:00
18f2e25628 Renamed migrations for database tables 2022-04-03 13:44:31 -05:00
369436e64f Npm now runs build script correctly 2022-04-03 12:47:34 -05:00
11e5369b3e Fixed release build 2022-04-03 12:34:41 -05:00
28722cbc8e Separated test database and development database 2022-04-03 12:15:52 -05:00
e12cc9f80b DIst script now destroys env before running 2022-04-03 12:12:55 -05:00
497f859738 Added scripts to remove .env from package 2022-04-03 12:04:52 -05:00
be2566dba0 Reworked client models to work with new backend 2022-04-03 12:01:11 -05:00
31053b2df6 Updated test suite for new models 2022-04-02 23:39:59 -05:00
abbab50366 Reworked routes to work with new models 2022-04-02 23:39:48 -05:00
bc324d2488 Reworked middleware to work with new models 2022-04-02 23:39:33 -05:00
98c4f879c5 Renamed migrations 2022-04-02 22:37:10 -05:00
f8385d048e Rename migrate scripts 2022-04-02 22:36:56 -05:00
9fdda99c64 Removed sync from development environment 2022-04-02 22:36:34 -05:00
ec4f8ab0f4 Fixed release draft to not use environment 2022-04-02 21:29:35 -05:00
132d88a48d Fixing release assets 2022-04-02 21:22:26 -05:00
1a2f7042d4
Fixed release assets to not auto-release (#38) 2022-04-02 21:01:07 -05:00
167 changed files with 12820 additions and 35302 deletions

12
.eslintrc.yml Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
{
"sqltools.connections": [],
"sqltools.useNodeRuntime": true
}

25
client/.eslintrc.yml Normal file
View file

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

41
client/.gitignore vendored
View file

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

View file

@ -1,30 +0,0 @@
{
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default"
},
"previewLimit": 50,
"server": "c4thebomb101.com",
"port": 3306,
"driver": "MySQL",
"name": "node-test-site",
"database": "NODE_TEST_SITE",
"username": "c4patino",
"password": "JW63^Hy4HcBh"
},
{
"mysqlOptions": {
"authProtocol": "default"
},
"previewLimit": 50,
"server": "c4thebomb101.com",
"port": 3306,
"driver": "MySQL",
"name": "test-db",
"database": "TEST_DB",
"username": "c4patino@gmail.com",
"password": "JW63^hy4HcBh"
}
]
}

22
client/Dockerfile Normal file
View file

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

14
client/Dockerfile.dev Normal file
View file

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

13
client/index.html Normal file
View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

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

View file

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

View file

@ -1,60 +0,0 @@
import React, { useState } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import Navbar from './components/Navbar';
import MainPage from './components/MainPage';
import Form from './components/utils/Form';
import Login from './components/forms/Login';
import Register from './components/forms/Register';
import EditUser from './components/forms/EditUser';
import Recover from './components/forms/Recover';
import Reset from './components/forms/Reset';
import OwnedOrgs from './components/orgs/OwnedOrgs';
import MemberOrgs from './components/orgs/MemberOrgs';
import OrgDashboard from './components/orgs/OrgDashboard';
import UpdateOrganization from './components/forms/UpdateOrganization';
function App() {
const [token, setToken] = useState(false);
return (
<React.Fragment>
<BrowserRouter>
<Routes>
<Route path='' element={<Navbar token={token} />}>
<Route index element={<MainPage />} />
<Route
path='login'
element={<Login setToken={setToken} />}
/>
<Route path='register' element={<Register />} />
<Route path='update' element={<EditUser />} />
<Route path='recover' element={<Form />}>
<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>
</BrowserRouter>
</React.Fragment>
);
}
export default App;

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,232 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import {
Button,
Typography,
Box,
Grid,
Stack,
TextField,
FormGroup,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import OrgUnit from './utils/OrgUnit';
import StackItem from './utils/StackItem';
const Form = styled(FormGroup)(({ theme }) => ({
[theme.breakpoints.only('xs')]: {
column: true,
},
}));
const FormField = styled(TextField)(({ theme }) => ({
[theme.breakpoints.only('xs')]: {
column: true,
width: '75%',
},
}));
const FormButton = styled(Button)(({ theme }) => ({
disableElevation: true,
color: theme.palette.primary.main,
[theme.breakpoints.only('xs')]: {
column: true,
width: '25%',
},
}));
function MainPage() {
const navigate = useNavigate();
const [ownedOrgs, setOwnedOrgs] = useState([]);
const [orgs, setOrgs] = useState([]);
const [form, setForm] = useState({
name: '',
id: '',
});
const linkStyle = { textDecoration: 'none', color: 'inherit' };
useEffect(() => {
if (Cookies.get('token')) {
getMemberOrgs();
getOwnedOrgs();
} else {
navigate('/login');
}
}, [navigate]);
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function getOwnedOrgs() {
await axios
.get(
`${process.env.REACT_APP_API_ROOT}/auth?token=${Cookies.get(
'token'
)}`
)
.then((response) => {
setOwnedOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
})
.catch((e) => {
console.log(e);
});
}
async function getMemberOrgs() {
await axios
.get(
`${
process.env.REACT_APP_API_ROOT
}/auth/orgs?token=${Cookies.get('token')}`
)
.then((response) => {
setOrgs(() =>
response.data.map((org) => {
return {
orgID: org.orgID,
orgName: org.orgName,
memberCount: org.memberCount,
createdAt: org.createdAt,
};
})
);
});
}
async function createOrg(e) {
e.preventDefault();
console.log('create');
await axios
.post(`${process.env.REACT_APP_API_ROOT}/org/create`, {
token: Cookies.get('token'),
orgName: form.name,
})
.then(() => {
getOwnedOrgs();
});
}
async function joinOrg(e) {
e.preventDefault();
await axios
.post(`${process.env.REACT_APP_API_ROOT}/org/${form.id}/add`, {
token: Cookies.get('token'),
})
.then(() => {
getMemberOrgs();
});
}
return (
<React.Fragment>
<Box sx={{ flexGrow: 1 }}>
<Grid
container
spacing={2}
alignItems='center'
style={{ height: '95vh' }}
>
<Grid item xs={0.5} sm={2} md={4} />
<Grid item xs={11} sm={8} md={4}>
<Stack spacing={2}>
<StackItem text='Join an Organization'>
<form onSubmit={joinOrg}>
<Form>
<FormField
label='Organization ID'
name='id'
value={form.id}
onChange={handleChange}
/>
<FormButton
type='submit'
variant='outlined'
>
Join Organization
</FormButton>
</Form>
</form>
</StackItem>
<StackItem text='Create an Organization'>
<form onSubmit={createOrg}>
<Form>
<FormField
label='Organization Name'
name='name'
value={form.name}
onChange={handleChange}
/>
<FormButton
type='submit'
variant='outlined'
>
Create Organization
</FormButton>
</Form>
</form>
</StackItem>
<StackItem>
<Typography variant='h6'>
<Link to='org' style={linkStyle}>
Your Organizations
</Link>
</Typography>
{ownedOrgs.length === 0 && (
<Typography variant='body2'>
You do own any organizations
</Typography>
)}
{ownedOrgs.map((org) => (
<OrgUnit org={org} />
))}
</StackItem>
<StackItem>
<Typography variant='h6'>
<Link to='org/joined' style={linkStyle}>
Joined Organizations
</Link>
</Typography>
{orgs.length === 0 && (
<Typography variant='body2'>
You are not a member in any
organizations
</Typography>
)}
{orgs.map((org) => (
<OrgUnit org={org} />
))}
</StackItem>
</Stack>
</Grid>
<Grid item xs={0.5} sm={2} md={4} />
</Grid>
</Box>
</React.Fragment>
);
}
export default MainPage;

View file

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

View file

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

View file

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

View file

@ -1,101 +0,0 @@
import React from 'react';
import { Link, Outlet } from 'react-router-dom';
import {
AppBar,
Button,
Toolbar,
Typography,
IconButton,
CssBaseline,
useMediaQuery,
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import { ThemeProvider, createTheme } from '@mui/material/styles';
function Navbar({ token }) {
const linkStyle = { textDecoration: 'none', color: 'inherit' };
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const darkTheme = createTheme({
palette: {
mode: prefersDarkMode ? 'dark' : 'light',
},
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
function ButtonGroup({ token }) {
if (token) {
return (
<React.Fragment>
<Button color='inherit'>
<Link to='/update' style={linkStyle}>
Account
</Link>
</Button>
<Button color='inherit'>
<Link to='/login' style={linkStyle}>
Logout
</Link>
</Button>
</React.Fragment>
);
} else {
return (
<React.Fragment>
<Button color='inherit'>
<Link to='/login' style={linkStyle}>
Login
</Link>
</Button>
<Button color='inherit'>
<Link to='/register' style={linkStyle}>
Register
</Link>
</Button>
</React.Fragment>
);
}
}
return (
<React.Fragment>
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<AppBar position='static'>
<Toolbar>
<IconButton
size='large'
edge='start'
color='inherit'
aria-label='menu'
sx={{ mr: 2 }}
>
<Link to='/' style={linkStyle}>
<MenuIcon />
</Link>
</IconButton>
<Typography
variant='h6'
component='div'
sx={{ flexGrow: 1 }}
>
KnowItAll
</Typography>
<ButtonGroup token={token} />
</Toolbar>
</AppBar>
<Outlet />
</ThemeProvider>
</React.Fragment>
);
}
export default Navbar;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,119 @@
import {
Button,
FormGroup,
Paper,
Stack,
TextField,
TextFieldProps,
ButtonProps,
} from "@mui/material";
import { styled } from "@mui/material/styles";
const Form = styled(FormGroup)(({ theme }) => ({
[theme.breakpoints.only("xs")]: {
column: true,
},
}));
const FormField = styled(TextField)(({ theme }) => ({
[theme.breakpoints.only("xs")]: {
column: true,
width: "75%",
},
}));
const FormButton = styled(Button)(({ theme }) => ({
disableElevation: true,
color: theme.palette.primary.main,
[theme.breakpoints.only("xs")]: {
column: true,
width: "25%",
},
}));
const DynamicStack = styled(Stack)(({ theme }) => ({
[theme.breakpoints.up("sm")]: {
spacing: 2,
alignItems: "flex-end",
},
}));
const DynamicPaper = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: "2vh",
textAlign: "center",
color: theme.palette.text.secondary,
flexGrow: 1,
margin: "0 2vh",
[theme.breakpoints.only("xs")]: {
height: "80vh",
},
[theme.breakpoints.only("sm")]: {
height: "80vh",
},
[theme.breakpoints.only("md")]: {
height: "82vh",
},
}));
interface FormTextFieldProps {
label: string;
name: string;
value: string;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
required?: boolean;
other?: TextFieldProps;
}
function FormTextField({
label,
name,
value,
onChange,
required,
other,
}: FormTextFieldProps) {
return (
<TextField
required={required}
fullWidth
label={label}
name={name}
variant="outlined"
onChange={onChange}
value={value}
sx={{ margin: "1vh 0vh" }}
{...other}
/>
);
}
interface FormSubmitProps {
color: "primary" | "secondary" | "error";
children: React.ReactNode;
type: "button" | "submit";
other?: ButtonProps;
}
function FormSubmit({ color, children, type, other }: FormSubmitProps) {
return (
<Button
variant="contained"
color={color}
sx={{ margin: "1vh 0vh" }}
type={type}
fullWidth
{...other}
>
{children}
</Button>
);
}
export {
DynamicPaper,
DynamicStack,
Form,
FormButton,
FormField,
FormSubmit,
FormTextField,
};

View file

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

View file

@ -1,148 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import { Box, Typography, TextField, Button } from '@mui/material';
import Form from '../utils/Form';
import RecordMp3 from '../utils/RecordMp3';
function EditUser() {
const navigate = useNavigate();
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
pronouns: '',
});
const [error, setError] = useState('');
const linkStyle = { textDecoration: 'none', color: 'inherit' };
useEffect(() => {
async function getInfo() {
axios
.get(
`${process.env.REACT_APP_API_ROOT}/auth/${Cookies.get(
'userID'
)}?token=${Cookies.get('token')}`
)
.then((response) => {
const user = response.data;
setForm(() => {
return {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
pronouns: user.pronouns,
};
});
});
}
getInfo();
}, []);
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.patch(`${process.env.REACT_APP_API_ROOT}/auth/update`, {
...form,
token: Cookies.get('token'),
})
.then(() => {
navigate('/');
})
.catch((e) => setError(() => e.response.data));
}
return (
<Form>
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Update Account Details
</Typography>
<TextField
required
fullWidth
label='First Name'
name='firstName'
variant='outlined'
onChange={handleChange}
value={form.firstName}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Last Name'
name='lastName'
variant='outlined'
onChange={handleChange}
value={form.lastName}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Email'
name='email'
variant='outlined'
onChange={handleChange}
value={form.email}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
fullWidth
label='Pronouns'
name='pronouns'
variant='outlined'
onChange={handleChange}
value={form.pronouns}
sx={{ margin: '1vh 0vh' }}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='secondary'>
{error}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
>
Update Details
</Button>
<Button
variant='contained'
color='error'
sx={{ margin: '1vh 0vh' }}
fullWidth
>
<Link to='/recover' style={linkStyle}>
Reset Your Password
</Link>
</Button>
</form>
<RecordMp3 />
</Form>
);
}
export default EditUser;

View file

@ -1,151 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import {
Box,
Typography,
TextField,
Button,
FormControlLabel,
Checkbox,
} from '@mui/material';
import Form from '../utils/Form';
function Login({ setToken }) {
const navigate = useNavigate();
const [form, setForm] = useState({
email: '',
password: '',
remember: false,
});
const [error, setError] = useState('');
useEffect(() => {
async function logout() {
await axios
.post(`${process.env.REACT_APP_API_ROOT}/auth/logout`, {
token: Cookies.get('token'),
})
.catch((e) => {
console.log(e);
});
Cookies.remove('token');
Cookies.remove('userID');
setToken(() => false);
}
logout();
}, [setToken]);
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
function handleCheckboxChange(e) {
const name = e.target.name;
const checked = e.target.checked;
setForm(() => {
return { ...form, [name]: checked };
});
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.post(`${process.env.REACT_APP_API_ROOT}/auth/login`, form)
.then((response) => {
Cookies.set('token', response.data.token, {
expires: form.remember ? 3650 : 1,
});
Cookies.set('userID', response.data.userID, {
expires: form.remember ? 3650 : 1,
});
setToken(true);
navigate('/');
})
.catch((e) => setError(() => e.response.data));
}
return (
<Form>
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Login
</Typography>
<TextField
required
fullWidth
label='Email'
name='email'
variant='outlined'
onChange={handleChange}
value={form.email}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Password'
name='password'
type='password'
variant='outlined'
onChange={handleChange}
value={form.password}
sx={{ margin: '1vh 0vh' }}
/>
<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>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='error'>
{error}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
>
Login
</Button>
<Typography variant='body1'>
Dont have an account? <Link to='/register'>Register</Link>
</Typography>
<Typography variant='body1'>
Forgot your password?{' '}
<Link to='/recover'>Recover Password</Link>
</Typography>
</form>
</Form>
);
}
export default Login;

View file

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

View file

@ -1,140 +0,0 @@
import React, { useState } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import axios from 'axios';
import { Box, Typography, TextField, Button } from '@mui/material';
import Form from '../utils/Form';
function Register() {
const navigate = useNavigate();
const [params] = useSearchParams();
const [form, setForm] = useState({
firstName: '',
lastName: '',
email: '',
pronouns: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
function handleChange(e) {
const name = e.target.name;
const value = e.target.value;
setForm((form) => {
return { ...form, [name]: value };
});
}
async function handleSubmit(e) {
e.preventDefault();
const { confirmPassword, ...filteredForm } = form;
await axios
.post(`${process.env.REACT_APP_API_ROOT}/auth/register`, {
...filteredForm,
orgID: params.orgID,
})
.then(() => {
navigate('/login');
})
.catch((e) => setError(() => e.response.data));
}
return (
<Form>
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Create an Account
</Typography>
<TextField
required
fullWidth
label='First Name'
name='firstName'
variant='outlined'
onChange={handleChange}
value={form.firstName}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Last Name'
name='lastName'
variant='outlined'
onChange={handleChange}
value={form.lastName}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Email'
name='email'
variant='outlined'
onChange={handleChange}
value={form.email}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
fullWidth
label='Pronouns'
name='pronouns'
variant='outlined'
onChange={handleChange}
value={form.pronouns}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Password'
name='password'
type='password'
variant='outlined'
onChange={handleChange}
value={form.password}
sx={{ margin: '1vh 0vh' }}
/>
<TextField
required
fullWidth
label='Confirm Password'
name='confirmPassword'
type='password'
variant='outlined'
onChange={handleChange}
value={form.confirmPassword}
sx={{ margin: '1vh 0vh' }}
/>
{error && (
<Box textAlign='right'>
<Typography variant='body2' color='secondary'>
{error}
</Typography>
</Box>
)}
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
disabled={form.password !== form.confirmPassword}
>
Create an Account
</Button>
<Typography variant='body1'>
Already have an account? <Link to='/login'>Login</Link>
</Typography>
</form>
</Form>
);
}
export default Register;

View file

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

View file

@ -1,66 +0,0 @@
import React, { useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import { Typography, TextField, Button } from '@mui/material';
import Form from '../utils/Form';
function UpdateOrganization() {
const { orgID } = useParams();
const navigate = useNavigate();
const [name, setName] = useState('');
function handleChange(e) {
const value = e.target.value;
setName(() => value);
}
async function handleSubmit(e) {
e.preventDefault();
await axios
.patch(`${process.env.REACT_APP_API_ROOT}/org/update`, {
token: Cookies.get('token'),
orgName: name,
orgID,
})
.then(() => {
navigate(`/org/${orgID}`);
});
}
return (
<Form>
<form onSubmit={handleSubmit}>
<Typography variant='h4' sx={{ marginTop: '1vh 0vh' }}>
Rename Organization
</Typography>
<TextField
required
fullWidth
label='Name'
name='name'
variant='outlined'
onChange={handleChange}
value={name}
sx={{ margin: '1vh 0vh' }}
/>
<Button
variant='contained'
color='primary'
sx={{ margin: '1vh 0vh' }}
type='submit'
fullWidth
>
Rename
</Button>
</form>
</Form>
);
}
export default UpdateOrganization;

View file

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

View file

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

View file

@ -1,283 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams, Link } from 'react-router-dom';
import axios from 'axios';
import Cookies from 'js-cookie';
import {
Typography,
Grid,
Stack,
Box,
ButtonGroup,
Button,
Accordion,
AccordionSummary,
AccordionDetails,
useMediaQuery,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Dashboard from '../utils/Dashboard';
const DynamicStack = styled(Stack)(({ theme }) => ({
[theme.breakpoints.up('sm')]: {
spacing: 2,
alignItems: 'flex-end',
},
}));
function OrgDashboard() {
const xs = useMediaQuery((theme) => theme.breakpoints.only('xs'));
const { orgID } = useParams();
const navigate = useNavigate();
const [org, setOrg] = useState({
orgID: orgID,
orgName: '',
orgOwner: {
firstName: '',
lastName: '',
email: '',
},
orgMembers: [],
createdAt: '',
memberCount: 0,
});
const [rows, setRows] = useState([]);
const [status, setStatus] = useState(true);
const [selection, setSelection] = useState(null);
const [open, setOpen] = useState(false);
const linkStyle = { textDecoration: 'none', color: 'inherit' };
const buttonGroupOrientation = () => {
if (xs) return 'vertical';
};
useEffect(() => {
async function getData() {
await axios
.get(
`${
process.env.REACT_APP_API_ROOT
}/org/${orgID}?token=${Cookies.get('token')}`
)
.then((response) => {
const res = response.data;
console.log(res);
setOrg(() => {
return {
orgID: res.org.orgID,
orgName: res.org.orgName,
orgOwner: {
firstName: res.org.orgOwner.firstName,
lastName: res.org.orgOwner.lastName,
email: res.org.orgOwner.email,
},
orgMembers: res.org.orgMember,
createdAt: res.org.createdAt,
memberCount: res.memberCount,
};
});
const orgMembers = res.org.orgMember.map((member) => {
return {
id: member.userID,
...member,
};
});
setRows(() => orgMembers);
setStatus(() => res.status);
})
.catch((e) => {
console.log(e);
navigate('/');
});
}
getData();
}, [navigate, orgID]);
async function handleDelete() {
await axios
.delete(`${process.env.REACT_APP_API_ROOT}/org/delete`, {
params: {
token: Cookies.get('token'),
orgID,
},
})
.then(navigate('/'));
}
function copyID() {
navigator.clipboard.writeText(orgID);
}
function copyJoinLink() {
navigator.clipboard.writeText(
`${process.env.REACT_APP_DOMAIN_ROOT}/regsiter?orgID=${orgID}`
);
}
async function removeSelected() {
const doomedUserIDs = selection.map((row) => row.id);
await axios.post(
`${process.env.REACT_APP_API_ROOT}/org/${orgID}/delete`,
{
token: Cookies.get('token'),
orgID,
doomedUserIDs,
}
);
}
async function leaveOrg() {
await axios
.post(`${process.env.REACT_APP_API_ROOT}/org/${orgID}/delete`, {
token: Cookies.get('token'),
orgID,
})
.then(() => navigate('/'))
.catch((e) => {
console.log(e);
navigate('/');
});
}
function handleOpen() {
setOpen((initial) => (initial ? false : true));
}
async function play(id) {
axios
.get(
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${id}.mp3`,
{
responseType: 'blob',
}
)
.then((res) => {
const uploadedFile = new File([res.data], 'userAudio.mp3', {
type: res.data.type,
lastModified: Date.now(),
});
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
audioFile.play();
});
}
return (
<Dashboard
rows={rows}
setSelection={setSelection}
onClick={play}
open={open}
>
<Accordion expanded={open} onChange={handleOpen}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant='h6'>
Organizations/{org.orgName}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ marginTop: '1vh' }} fullWidth>
<Grid
container
columns={{ xs: 3, sm: 12, md: 12 }}
spacing={1}
>
<Grid item xs={3}>
<Stack>
<Typography variant='body1'>
Organization ID: {org.orgID}
</Typography>
<Typography variant='body1'>
Members: {org.memberCount}
</Typography>
<Typography variant='body1'>
Created On: {org.createdAt}
</Typography>
</Stack>
</Grid>
<Grid item xs={3}>
<Stack>
<Typography variant='body1'>
Owner:{' '}
{`${org.orgOwner.firstName} ${org.orgOwner.lastName}`}
</Typography>
<Typography variant='body1'>
Email: {org.orgOwner.email}
</Typography>
</Stack>
</Grid>
<Grid item xs={3} sm={6} md={6}>
<DynamicStack spacing={1}>
{status ? (
<React.Fragment>
<ButtonGroup
variant='outlined'
orientation={buttonGroupOrientation()}
>
<Button color='warning'>
<Link
to='update'
style={linkStyle}
>
Edit {org.orgName}
</Link>
</Button>
<Button
color='error'
onClick={handleDelete}
>
Delete {org.orgName}
</Button>
</ButtonGroup>
<ButtonGroup
variant='outlined'
orientation={buttonGroupOrientation()}
>
<Button
color='success'
onClick={copyID}
>
Copy Org ID
</Button>
<Button
color='success'
onClick={copyJoinLink}
>
Copy Org Join Link
</Button>
<Button
color='error'
onClick={removeSelected}
>
Remove People
</Button>
</ButtonGroup>
</React.Fragment>
) : (
<Button
color='error'
onClick={leaveOrg}
>
Leave Org
</Button>
)}
</DynamicStack>
</Grid>
</Grid>
</Box>
</AccordionDetails>
</Accordion>
</Dashboard>
);
}
export default OrgDashboard;

View file

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

View file

@ -1,104 +0,0 @@
import React from 'react';
import { Paper, Box, IconButton, useMediaQuery, Collapse } from '@mui/material';
import { styled } from '@mui/material/styles';
import { DataGrid } from '@mui/x-data-grid';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import MemberUnit from '../utils/MemberUnit';
const DynamicPaper = styled(Paper)(({ theme }) => ({
...theme.typography.body2,
padding: '2vh',
textAlign: 'center',
color: theme.palette.text.secondary,
flexGrow: 1,
margin: '0 2vh',
[theme.breakpoints.only('xs')]: {
height: '80vh',
},
[theme.breakpoints.only('sm')]: {
height: '80vh',
},
[theme.breakpoints.only('md')]: {
height: '82vh',
},
}));
function Dashboard({ children, rows, setSelection, onClick, open }) {
const xs = useMediaQuery((theme) => theme.breakpoints.only('xs'));
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
const columns = [
{ field: 'id', headerName: 'ID', flex: 1 },
{ field: 'firstName', headerName: 'First Name', flex: 3 },
{ field: 'lastName', headerName: 'Last Name', flex: 3 },
{ field: 'nickname', headerName: 'Nickname', flex: 2 },
{
field: 'namePronounciation',
headerName: 'Pronounciation',
flex: 2,
sortable: false,
renderCell: (params) => (
<IconButton
size='large'
edge='start'
color='inherit'
aria-label='menu'
sx={{ mr: 2 }}
onClick={() => onClick(params.id)}
>
<PlayArrowIcon />
</IconButton>
),
},
{ field: 'pronouns', headerName: 'Pronouns', flex: 2 },
{ field: 'email', headerName: 'Email', flex: 10, sortable: false },
];
function calculateCollapsedSize() {
if (xs) return '37vh';
if (sm) return '63vh';
if (md) return '68vh';
}
function calculateTableHeight() {
return open ? '64vh' : '100%';
}
return (
<React.Fragment>
<Box sx={{ padding: '2vh' }}>{children}</Box>
<Collapse
collapsedSize={calculateCollapsedSize()}
in={open ? false : true}
>
<DynamicPaper>
{(xs || sm) &&
rows.map((member) => {
return (
<MemberUnit member={member} onClick={onClick} />
);
})}
{md && (
<Box height={calculateTableHeight()}>
<DataGrid
rows={rows}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
onSelectionModelChange={(newSelection) => {
setSelection(() => newSelection.rows);
}}
checkboxSelection
/>
</Box>
)}
</DynamicPaper>
</Collapse>
</React.Fragment>
);
}
export default Dashboard;

View file

@ -1,40 +0,0 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { Box, Grid, Paper } from '@mui/material';
import { useTheme } from '@mui/material/styles';
function Form({ children }) {
const theme = useTheme();
return (
<React.Fragment>
<Box sx={{ flexGrow: 1 }}>
<Grid
container
spacing={2}
alignItems='center'
style={{ height: '95vh' }}
>
<Grid item xs={0.5} sm={2} md={4} />
<Grid item xs={11} sm={8} md={4}>
<Paper
sx={{
...theme.typography.body2,
padding: '2vh',
textAlign: 'center',
color: theme.palette.text.secondary,
}}
>
{children}
<Outlet />
</Paper>
</Grid>
<Grid item xs={0.5} sm={2} md={4} />
</Grid>
</Box>
</React.Fragment>
);
}
export default Form;

View file

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

View file

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

View file

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

View file

@ -1,128 +0,0 @@
import React, { useState, useEffect } from 'react';
import MicRecorder from 'mic-recorder-to-mp3';
import axios from 'axios';
import Cookies from 'js-cookie';
import { IconButton } from '@mui/material';
import PlayArrow from '@mui/icons-material/PlayArrow';
import Mic from '@mui/icons-material/Mic';
import Upload from '@mui/icons-material/Upload';
import SaveAs from '@mui/icons-material/SaveAs';
function RecordMp3() {
const [recorder] = useState(
new MicRecorder({
bitRate: 128,
})
);
const [recording, setRecording] = useState(false);
const [uploadedFile, setUploadedFile] = useState(null);
const [audio, setAudio] = useState(null);
useEffect(() => {
axios
.get(
`${
process.env.REACT_APP_DOMAIN_ROOT
}/public/audio/${Cookies.get('userID')}.mp3`,
{
responseType: 'blob',
}
)
.then((res) => {
setUploadedFile(() => {
return new File([res.data], 'userAudio.mp3', {
type: res.data.type,
lastModified: Date.now(),
});
});
});
}, []);
function togglePlay() {
if (audio && !audio.ended) {
audio.pause();
audio.currentTime = 0;
setAudio(() => null);
} else {
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
setAudio(() => 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().catch((e) => {
console.error(e);
});
}
}
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');
formData.append('token', Cookies.get('token'));
await axios.post(
`${process.env.REACT_APP_API_ROOT}/auth/audio`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
}
);
}
return (
<form onSubmit={handleSubmit}>
<IconButton onClick={togglePlay}>
<PlayArrow />
</IconButton>
<IconButton onClick={record}>
<Mic />
</IconButton>
<input
id='icon-button-file'
type='file'
accept='audio/*'
hidden
onChange={uploadRecord}
/>
<label htmlFor='icon-button-file'>
<IconButton component='span'>
<Upload />
</IconButton>
</label>
<IconButton type='submit'>
<SaveAs />
</IconButton>
</form>
);
}
export default RecordMp3;

View file

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

View file

@ -0,0 +1,16 @@
import Helmet from "react-helmet";
import { EditUserForm } from "../controllers";
function Login() {
return (
<>
<Helmet>
<title>Edit User | KnowItAll</title>
</Helmet>
<EditUserForm />
</>
);
}
export default Login;

View file

@ -0,0 +1,49 @@
import { Box, CssBaseline } from "@mui/material";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { Helmet } from "react-helmet";
import { Outlet } from "react-router-dom";
import { useMode } from "../contexts";
import { Navbar } from "../controllers";
declare module "@mui/material/styles" {
interface BreakpointOverrides {
xs: true;
sm: true;
md: true;
lg: false;
xl: false;
}
}
function Layout() {
const { mode } = useMode();
const theme = createTheme({
palette: { mode },
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<Helmet>
<title>KnowItAll</title>
</Helmet>
<main>
<Navbar />
<Box sx={{ height: { xs: "", sm: "92vh", md: "92vh" } }}>
<Outlet />
</Box>
</main>
</ThemeProvider>
);
}
export default Layout;

View file

@ -0,0 +1,16 @@
import Helmet from "react-helmet";
import { LoginForm } from "../controllers";
function Login() {
return (
<>
<Helmet>
<title>Login | KnowItAll</title>
</Helmet>
<LoginForm />
</>
);
}
export default Login;

View file

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

View file

@ -0,0 +1,14 @@
import Helmet from "react-helmet";
import { MemberOrgsDashboard } from "../controllers";
export default function MemberOrgs() {
return (
<>
<Helmet>
<title>Member Organizations | KnowItAll</title>
</Helmet>
<MemberOrgsDashboard />
</>
);
}

View file

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

View file

@ -0,0 +1,16 @@
import Helmet from "react-helmet";
import { OwnedOrgsDashboard } from "../controllers";
function OwnedOrgs() {
return (
<>
<Helmet>
<title>Owned Organizations | KnowItAll</title>
</Helmet>
<OwnedOrgsDashboard />
</>
);
}
export default OwnedOrgs;

View file

@ -0,0 +1,16 @@
import Helmet from "react-helmet";
import { RecoverForm } from "../controllers";
function Login() {
return (
<>
<Helmet>
<title>Recover Password | KnowItAll</title>
</Helmet>
<RecoverForm />
</>
);
}
export default Login;

View file

@ -0,0 +1,16 @@
import Helmet from "react-helmet";
import { RegisterForm } from "../controllers";
function Register() {
return (
<>
<Helmet>
<title>Register | KnowItAll</title>
</Helmet>
<RegisterForm />
</>
);
}
export default Register;

View file

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

View file

@ -0,0 +1,14 @@
import Helmet from "react-helmet";
import { UpdateOrganizationForm } from "../controllers";
export default function UpdateOrganization() {
return (
<>
<Helmet>
<title>Update Organization | KnowItAll</title>
</Helmet>
<UpdateOrganizationForm />
</>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
/>
);
}

View 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
View file

@ -0,0 +1,8 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
REACT_APP_DOMAIN_ROOT: string;
REACT_APP_API_ROOT: string;
}
}
}

View file

@ -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
View 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>
);

View file

@ -0,0 +1,7 @@
export type APIError = {
response: {
data: {
error: string;
};
};
};

View file

@ -0,0 +1,10 @@
import { User } from "./UserTypes";
export type Organization = {
id: string;
name: string;
owner: User;
members: string[];
createdAt: string;
memberCount: number;
};

View file

@ -0,0 +1 @@
export type ThemeMode = "light" | "dark";

View file

@ -0,0 +1,10 @@
export type User = {
id: number;
firstName: string;
lastName: string;
username: string;
pronouns: string;
nickname: string;
email: string;
age: number;
};

View file

@ -0,0 +1,4 @@
export type { ThemeMode } from "./ThemeTypes";
export type { User } from "./UserTypes";
export type { Organization } from "./OrganizationTypes";
export type { APIError } from "./ErrorTypes";

View file

@ -0,0 +1,15 @@
import axios from "axios";
import Cookies from "js-cookie";
function createRequest() {
const request = axios.create({
baseURL: `${process.env.REACT_APP_API_ROOT}`,
});
request.defaults.headers.common["Content-Type"] = "application/json";
request.defaults.headers.common.Authorization = `bearer ${Cookies.get("token")}`;
return request;
}
export default createRequest;

25
client/tsconfig.json Normal file
View file

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
client/tsconfig.node.json Normal file
View file

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

6
client/vite.config.ts Normal file
View file

@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [react()],
});

38
docker-compose.prod.yml Normal file
View file

@ -0,0 +1,38 @@
version: '3.8'
services:
client:
restart: unless-stopped
build:
context: ./client
platforms:
- linux/amd64
- linux/arm64/v8
image: c4thebomb/know-it-all-client
ports:
- 127.0.0.1:3000:3000
networks:
- know-it-all
server:
restart: unless-stopped
image: c4thebomb/know-it-all-server
build:
context: ./server
platforms:
- linux/amd64
- linux/arm64/v8
ports:
- 127.0.0.1:3001:3000
networks:
- know-it-all
- common-network
env_file:
- path: .env
required: true
environment:
CHOKIDAR_USEPOLLING: true
networks:
common-network:
external: true
know-it-all: {}

57
docker-compose.yml Normal file
View file

@ -0,0 +1,57 @@
version: '3.8'
services:
client:
restart: unless-stopped
build:
context: ./client
dockerfile: Dockerfile.dev
ports:
- 3000:3000
volumes:
- ./client:/app1
- /app/node_modules
networks:
- know-it-all
environment:
REACT_APP_API_ROOT: 'http://localhost:3001/api'
REACT_APP_DOMAIN_ROOT: 'http://localhost:3000'
WATCHPACK_POLLING: true
command: npm start
server:
restart: unless-stopped
build:
context: ./server
dockerfile: Dockerfile.dev
ports:
- 3001:3000
volumes:
- ./server:/app
- /app/node_modules
networks:
- know-it-all
environment:
NODE_ENV: development
depends_on:
- mysqldb
mysqldb:
restart: unless-stopped
image: mysql:8.3
ports:
- 3306:3306
volumes:
- know-it-all-mysql:/var/lib/mysql
networks:
- know-it-all
environment:
MYSQL_ROOT_PASSWORD: 'passw0rd'
MYSQL_DATABASE: dev
healthcheck:
test: [ 'CMD', 'mysqladmin', 'ping', '-h', 'localhost' ]
timeout: 5s
retries: 10
volumes:
know-it-all-mysql:
networks:
know-it-all: {}

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