Compare commits
65 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df862da6b2 | |||
| db1e41b949 | |||
| 17590cc43d | |||
| 1f9a3acc17 | |||
| 7ee60242d3 | |||
| 5d2714e1ee | |||
| 720e4f37b0 | |||
| c099f016f9 | |||
| 469204a30f | |||
| fb43f05d8a | |||
| ff255baf15 | |||
| bc42b39b0c | |||
| a75305be1c | |||
| 2dddd857c1 | |||
| dc5b510365 | |||
| d8becd3af4 | |||
| f6c739bfe1 | |||
| 9e8e074181 | |||
| 7ca9067533 | |||
| f5ad8e5f8d | |||
| c57c2b35c5 | |||
| 52b76a760c | |||
| 6d7d0fc216 | |||
| 586567690d | |||
| 8f63aea8b6 | |||
| 7a0ae20539 | |||
| b7b3b910ff | |||
| 73be3993a0 | |||
| c631e4e6b5 | |||
| 9d1338c6c0 | |||
| 266892bb26 | |||
| c0a6be7c63 | |||
| 7437610b93 | |||
| b62d177654 | |||
| 62a0cc96cc | |||
| 18f2e25628 | |||
| 369436e64f | |||
| 11e5369b3e | |||
| 28722cbc8e | |||
| e12cc9f80b | |||
| 497f859738 | |||
| be2566dba0 | |||
| 31053b2df6 | |||
| abbab50366 | |||
| bc324d2488 | |||
| 98c4f879c5 | |||
| f8385d048e | |||
| 9fdda99c64 | |||
| ec4f8ab0f4 | |||
| 132d88a48d | |||
|
1a2f7042d4 |
|||
|
d1c825d79b |
|||
|
4d7e1bea8e |
|||
|
8ec96a53de |
|||
|
38c5b1cf78 |
|||
|
57fff6a5e2 |
|||
|
79ff74fff0 |
|||
|
65b8a742a6 |
|||
|
a977c6b296 |
|||
|
3480f8379d |
|||
|
47513f08db |
|||
|
4a2d04b3a0 |
|||
|
b2e727e7bf |
|||
|
7dd9d244fa |
|||
|
1b9b8db04d |
169 changed files with 12872 additions and 35190 deletions
12
.eslintrc.yml
Normal file
12
.eslintrc.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
env:
|
||||
browser: true
|
||||
es2021: true
|
||||
extends: [airbnb, "prettier"]
|
||||
parserOptions:
|
||||
ecmaVersion: latest
|
||||
sourceType: module
|
||||
project: ./tsconfig.json
|
||||
ignorePatterns: ["vite.config.js"]
|
||||
rules:
|
||||
indent: ["error", 4]
|
||||
comma-dangle: "off"
|
||||
31
.github/release-drafter.yml
vendored
31
.github/release-drafter.yml
vendored
|
|
@ -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
|
||||
46
.github/workflows/build-release.yml
vendored
46
.github/workflows/build-release.yml
vendored
|
|
@ -1,46 +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: [main]
|
||||
|
||||
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: Write config.json to config
|
||||
env:
|
||||
CONFIG_JSON: '${{ secrets.CONFIG_JSON }}'
|
||||
run: |
|
||||
mkdir ./server/config
|
||||
echo "$CONFIG_JSON" >> ./server/config/config.json
|
||||
- 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: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.release-draft.outputs.upload_url }}
|
||||
asset_path: ./dist.tar.gz
|
||||
asset_name: dist.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
38
.github/workflows/test-build.yml
vendored
38
.github/workflows/test-build.yml
vendored
|
|
@ -1,38 +0,0 @@
|
|||
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: 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: Write config.json to config
|
||||
env:
|
||||
CONFIG_JSON: '${{ secrets.CONFIG_JSON }}'
|
||||
run: |
|
||||
mkdir ./server/config
|
||||
echo "$CONFIG_JSON" >> ./server/config/config.json
|
||||
- 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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,7 +12,6 @@ TODO
|
|||
|
||||
### Sqlite ###
|
||||
*.sqlite3
|
||||
**/config/
|
||||
|
||||
### Git ###
|
||||
# Created by git for backups. To disable backups in Git:
|
||||
|
|
|
|||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"sqltools.connections": [],
|
||||
"sqltools.useNodeRuntime": true
|
||||
}
|
||||
25
README.md
25
README.md
|
|
@ -0,0 +1,25 @@
|
|||
## Defining .env file
|
||||
|
||||
---
|
||||
|
||||
### React client environment variables
|
||||
|
||||
- REACT_APP_API_ROOT: Domain root for react app api
|
||||
- REACT_APP_DOMAIN_ROOT: Domain root for react app
|
||||
- BUILD_PATH: Path for client build folder
|
||||
|
||||
### Courier client
|
||||
|
||||
- COURIER_AUTH_TOKEN: Authorization token for courier client
|
||||
|
||||
### Node environment variables
|
||||
|
||||
- NODE_ENV: Node environment
|
||||
- PORT: Default node port
|
||||
|
||||
### Production database information
|
||||
|
||||
- DB_USERNAME: Production database username
|
||||
- DB_PASSWORD: Production database password
|
||||
- DB_HOST: Production database host ip
|
||||
- DB_NAME: Production database name
|
||||
25
client/.eslintrc.yml
Normal file
25
client/.eslintrc.yml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
root: false
|
||||
env: { browser: true, es2020: true }
|
||||
extends:
|
||||
- "eslint:recommended"
|
||||
- "airbnb-typescript"
|
||||
- "plugin:@typescript-eslint/recommended"
|
||||
- "plugin:react-hooks/recommended"
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs"]
|
||||
parser: "@typescript-eslint/parser"
|
||||
plugins: ["react-refresh"]
|
||||
rules:
|
||||
"react-refresh/only-export-components":
|
||||
["warn", { allowConstantExport: true }]
|
||||
"react/jsx-indent": ["error", 4]
|
||||
"react/jsx-indent-props": ["error", 4]
|
||||
"react/jsx-props-no-spreading": "off"
|
||||
"react/react-in-jsx-scope": "off"
|
||||
"react/require-default-props": "off"
|
||||
"@typescript-eslint/indent": ["error", 4]
|
||||
"@typescript-eslint/quotes": ["error", "double"]
|
||||
"@typescript-eslint/no-shadow": "off"
|
||||
"@typescript-eslint/comma-dangle": "off"
|
||||
"import/no-extraneous-dependencies": "off"
|
||||
"import/no-cycle": "off"
|
||||
"jsx-a11y/label-has-associated-control": "off"
|
||||
41
client/.gitignore
vendored
41
client/.gitignore
vendored
|
|
@ -1,23 +1,24 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# 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?
|
||||
|
|
|
|||
30
client/.vscode/settings.json
vendored
30
client/.vscode/settings.json
vendored
|
|
@ -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
22
client/Dockerfile
Normal 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
14
client/Dockerfile.dev
Normal 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
13
client/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,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 |
|
|
@ -1,42 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>KnowItAll</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
--></body>
|
||||
</html>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
|
|
@ -1,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
56
client/src/App.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import {
|
||||
BrowserRouter, Routes, Route, Navigate,
|
||||
} from "react-router-dom";
|
||||
|
||||
import {
|
||||
Layout,
|
||||
MainPage,
|
||||
Login,
|
||||
Register,
|
||||
EditUser,
|
||||
Recover,
|
||||
Reset,
|
||||
OwnedOrgs,
|
||||
MemberOrgs,
|
||||
OrgDashboard,
|
||||
UpdateOrganization,
|
||||
} from "./containers";
|
||||
import { ContextProvider } from "./contexts";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ContextProvider>
|
||||
<Routes>
|
||||
<Route path="" element={<Layout />}>
|
||||
<Route index element={<MainPage />} />
|
||||
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/update" element={<EditUser />} />
|
||||
<Route path="recover">
|
||||
<Route index element={<Recover />} />
|
||||
<Route path=":id" element={<Reset />} />
|
||||
</Route>
|
||||
|
||||
<Route path="org">
|
||||
<Route index element={<OwnedOrgs />} />
|
||||
<Route path="joined" element={<MemberOrgs />} />
|
||||
<Route path=":orgID">
|
||||
<Route index element={<OrgDashboard />} />
|
||||
<Route
|
||||
path="update"
|
||||
element={<UpdateOrganization />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate to="" />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ContextProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
229
client/src/components/Dashboard.tsx
Normal file
229
client/src/components/Dashboard.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
client/src/components/EditUserForm.tsx
Normal file
80
client/src/components/EditUserForm.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Box, Typography } from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Form from "./Form";
|
||||
import { RecordMp3 } from "../controllers";
|
||||
import { FormSubmit, FormTextField } from "./StyledElements";
|
||||
|
||||
interface EditUserFormProps {
|
||||
form: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
pronouns: string;
|
||||
};
|
||||
error: string;
|
||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
function EditUserForm({
|
||||
form,
|
||||
error,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
}: EditUserFormProps) {
|
||||
const linkStyle = { textDecoration: "none", color: "inherit" };
|
||||
|
||||
return (
|
||||
<Form text="Update Account Details">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormTextField
|
||||
required
|
||||
label="First Name"
|
||||
name="firstName"
|
||||
onChange={handleChange}
|
||||
value={form.firstName}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Last Name"
|
||||
name="lastName"
|
||||
onChange={handleChange}
|
||||
value={form.lastName}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Email"
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={form.email}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Pronouns"
|
||||
name="pronouns"
|
||||
onChange={handleChange}
|
||||
value={form.pronouns}
|
||||
/>
|
||||
{error && (
|
||||
<Box textAlign="right">
|
||||
<Typography variant="body2" color="secondary">
|
||||
{error}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<FormSubmit color="primary" type="submit">
|
||||
Update Details
|
||||
</FormSubmit>
|
||||
<FormSubmit color="error" type="button">
|
||||
<Link to="/recover" style={linkStyle}>
|
||||
Reset Your Password
|
||||
</Link>
|
||||
</FormSubmit>
|
||||
</form>
|
||||
<RecordMp3 />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditUserForm;
|
||||
58
client/src/components/Form.tsx
Normal file
58
client/src/components/Form.tsx
Normal 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;
|
||||
77
client/src/components/LoginForm.tsx
Normal file
77
client/src/components/LoginForm.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { Checkbox, FormControlLabel, Typography } from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Form from "./Form";
|
||||
import { FormSubmit, FormTextField } from "./StyledElements";
|
||||
|
||||
interface LoginProps {
|
||||
form: {
|
||||
email: string;
|
||||
password: string;
|
||||
remember: boolean;
|
||||
};
|
||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
handleChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleCheckboxChange: (event: React.SyntheticEvent<Element, Event>) => void;
|
||||
error: string;
|
||||
}
|
||||
|
||||
function Login({
|
||||
form,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleCheckboxChange,
|
||||
error,
|
||||
}: LoginProps) {
|
||||
return (
|
||||
<Form text="Login" error={error}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormTextField
|
||||
required
|
||||
label="Email"
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={form.email}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Password"
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
value={form.password}
|
||||
other={{ type: "password" }}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={form.remember} />}
|
||||
labelPlacement="start"
|
||||
label="Remember me"
|
||||
name="remember"
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
<FormSubmit color="primary" type="submit">
|
||||
Login
|
||||
</FormSubmit>
|
||||
<Typography variant="body1">
|
||||
Dont have an account?
|
||||
{" "}
|
||||
<Link to="/register">Register</Link>
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Forgot your password?
|
||||
{" "}
|
||||
<Link to="/recover">Recover Password</Link>
|
||||
</Typography>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
|
@ -1,224 +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 OrgUnit from './utils/OrgUnit';
|
||||
import StackItem from './utils/StackItem';
|
||||
|
||||
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')) {
|
||||
navigate('/login');
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getMemberOrgs();
|
||||
getOwnedOrgs();
|
||||
}, []);
|
||||
|
||||
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(() => {
|
||||
navigate('');
|
||||
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={1} sm={3} md={3.5} />
|
||||
<Grid item xs={10} sm={6} md={5}>
|
||||
<Stack spacing={2}>
|
||||
<StackItem text='Join an Organization'>
|
||||
<form onSubmit={joinOrg}>
|
||||
<FormGroup row>
|
||||
<TextField
|
||||
label='Organization ID'
|
||||
name='id'
|
||||
value={form.id}
|
||||
onChange={handleChange}
|
||||
sx={{ width: '75%' }}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
disableElevation
|
||||
sx={{ width: '25%' }}
|
||||
>
|
||||
Join Organization
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</StackItem>
|
||||
<StackItem text='Create an Organization'>
|
||||
<form onSubmit={createOrg}>
|
||||
<FormGroup row>
|
||||
<TextField
|
||||
label='Organization Name'
|
||||
name='name'
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
sx={{ width: '70%' }}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
disableElevation
|
||||
sx={{ width: '30%' }}
|
||||
>
|
||||
Create Organization
|
||||
</Button>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Typography variant='h6'>
|
||||
<Link to='org' style={linkStyle}>
|
||||
Your Organizations
|
||||
</Link>
|
||||
</Typography>
|
||||
{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={1} sm={3} md={3.5} />
|
||||
</Grid>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainPage;
|
||||
116
client/src/components/MainPage.tsx
Normal file
116
client/src/components/MainPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
client/src/components/MemberUnit.tsx
Normal file
57
client/src/components/MemberUnit.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { User } from "../types";
|
||||
|
||||
interface MemberUnitProps {
|
||||
member: User;
|
||||
onClick: (id: number) => void;
|
||||
}
|
||||
|
||||
export default function MemberUnit({ member, onClick }: MemberUnitProps) {
|
||||
const { id, firstName, lastName, nickname, pronouns, email } = member;
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>
|
||||
{firstName} {lastName} ({nickname}
|
||||
): {pronouns}
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item textAlign="left" xs={11} sm={11}>
|
||||
<Typography variant="body1">
|
||||
ID:
|
||||
{id}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Email:
|
||||
{email}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={1} sm={1} justifyContent="flex-end">
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => onClick(member.id)}
|
||||
>
|
||||
<PlayArrowIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
125
client/src/components/MobileNavbar.tsx
Normal file
125
client/src/components/MobileNavbar.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import AppRegistrationIcon from "@mui/icons-material/AppRegistration";
|
||||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import LoginIcon from "@mui/icons-material/Login";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import {
|
||||
AppBar,
|
||||
Box,
|
||||
Button,
|
||||
Drawer,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { User, ThemeMode } from "../types";
|
||||
|
||||
interface ControlButtonsProps {
|
||||
userData: User;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
function ControlButtons({ userData, logout }: ControlButtonsProps) {
|
||||
return userData.username ? (
|
||||
<ListItem>
|
||||
<ListItemButton
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to="/login"
|
||||
onClick={logout}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Logout</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
) : (
|
||||
<>
|
||||
<ListItem>
|
||||
<ListItemButton color="inherit" component={Link} to="/login">
|
||||
<ListItemIcon>
|
||||
<LoginIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Login</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemButton color="inherit" component={Link} to="/register">
|
||||
<ListItemIcon>
|
||||
<AppRegistrationIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Register</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface MobileNavbarProps {
|
||||
userData: User;
|
||||
logout: () => void;
|
||||
open: boolean;
|
||||
toggleOpen: () => void;
|
||||
mode: ThemeMode;
|
||||
toggleMode: () => void;
|
||||
}
|
||||
|
||||
export default function MobileNavbar({
|
||||
logout,
|
||||
userData,
|
||||
open,
|
||||
toggleOpen,
|
||||
mode,
|
||||
toggleMode,
|
||||
}: MobileNavbarProps) {
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{ margin: "0 1vw 0 0 " }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" component="div">
|
||||
KnowItAll
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
<Button color="inherit" onClick={toggleOpen}>
|
||||
Menu
|
||||
</Button>
|
||||
</Toolbar>
|
||||
<Drawer anchor="right" open={open} onClose={toggleOpen}>
|
||||
<List>
|
||||
<ControlButtons userData={userData} logout={logout} />
|
||||
<ListItem>
|
||||
<ListItemButton onClick={toggleMode}>
|
||||
<ListItemIcon>
|
||||
{mode === "light" ? (
|
||||
<DarkModeIcon />
|
||||
) : (
|
||||
<LightModeIcon />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
Switch to {mode === "light" ? "dark" : "light"}{" "}
|
||||
mode
|
||||
</ListItemText>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Drawer>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link, Outlet } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
AppBar,
|
||||
Button,
|
||||
Toolbar,
|
||||
Typography,
|
||||
IconButton,
|
||||
CssBaseline,
|
||||
} 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 darkTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
88
client/src/components/Navbar.tsx
Normal file
88
client/src/components/Navbar.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import {
|
||||
AppBar,
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Toolbar,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { User, ThemeMode } from "../types";
|
||||
|
||||
interface NavbarControlsProps {
|
||||
userData: User;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
function NavbarControls({ userData, logout }: NavbarControlsProps) {
|
||||
return userData.email ? (
|
||||
<>
|
||||
<Button color="inherit" component={Link} to="/update">
|
||||
Account
|
||||
</Button>
|
||||
<Button
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to="/login"
|
||||
onClick={logout}
|
||||
sx={{ margin: "0 1vh" }}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button color="inherit" component={Link} to="/login">
|
||||
Login
|
||||
</Button>
|
||||
<Button color="inherit" component={Link} to="/register">
|
||||
Register
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface NavbarProps {
|
||||
userData: User;
|
||||
logout: () => void;
|
||||
mode: ThemeMode;
|
||||
toggleMode: () => void;
|
||||
}
|
||||
|
||||
export default function Navbar({
|
||||
userData,
|
||||
logout,
|
||||
mode,
|
||||
toggleMode,
|
||||
}: NavbarProps) {
|
||||
return (
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
component={Link}
|
||||
to="/"
|
||||
sx={{ margin: "1vh 1vw 1vh 0" }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" component="div">
|
||||
KnowItAll
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={toggleMode}
|
||||
sx={{ margin: "0 1vh" }}
|
||||
>
|
||||
{mode === "light" ? <DarkModeIcon /> : <LightModeIcon />}
|
||||
</IconButton>
|
||||
<NavbarControls userData={userData} logout={logout} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
44
client/src/components/OrgUnit.tsx
Normal file
44
client/src/components/OrgUnit.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Organization } from "../types";
|
||||
|
||||
interface OrgUnitProps {
|
||||
org: Organization;
|
||||
}
|
||||
|
||||
function OrgUnit({ org }: OrgUnitProps) {
|
||||
const { id, name, memberCount, createdAt } = org;
|
||||
|
||||
const linkStyle = { textDecoration: "none", color: "inherit" };
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>
|
||||
<Link to={`/org/${id}`} style={linkStyle}>
|
||||
{name}
|
||||
</Link>
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body1">
|
||||
Members:
|
||||
{memberCount}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Created On:
|
||||
{createdAt}
|
||||
</Typography>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrgUnit;
|
||||
35
client/src/components/OrganizationList.tsx
Normal file
35
client/src/components/OrganizationList.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Box, Paper, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
import OrgUnit from "./OrgUnit";
|
||||
|
||||
import { Organization } from "../types";
|
||||
|
||||
interface OrganizationListProps {
|
||||
orgs: Organization[];
|
||||
}
|
||||
|
||||
export default function OrganizationList({ orgs }: OrganizationListProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
padding: "2vh",
|
||||
color: theme.palette.text.secondary,
|
||||
flexGrow: 1,
|
||||
height: "89vh",
|
||||
margin: "2vh",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">Organizations</Typography>
|
||||
<hr />
|
||||
<Box sx={{ overflowY: "auto" }}>
|
||||
{orgs.map((org) => (
|
||||
<OrgUnit org={org} key={org.id} />
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
47
client/src/components/RecordMp3.tsx
Normal file
47
client/src/components/RecordMp3.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import Mic from "@mui/icons-material/Mic";
|
||||
import PlayArrow from "@mui/icons-material/PlayArrow";
|
||||
import SaveAs from "@mui/icons-material/SaveAs";
|
||||
import Upload from "@mui/icons-material/Upload";
|
||||
import { IconButton } from "@mui/material";
|
||||
|
||||
interface RecordMp3Props {
|
||||
togglePlay: () => void;
|
||||
record: () => void;
|
||||
uploadRecord: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||
}
|
||||
|
||||
function RecordMp3({
|
||||
togglePlay,
|
||||
record,
|
||||
uploadRecord,
|
||||
handleSubmit,
|
||||
}: RecordMp3Props) {
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<IconButton onClick={togglePlay}>
|
||||
<PlayArrow />
|
||||
</IconButton>
|
||||
<IconButton onClick={record}>
|
||||
<Mic />
|
||||
</IconButton>
|
||||
<input
|
||||
id="icon-button-file"
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
hidden
|
||||
onChange={uploadRecord}
|
||||
/>
|
||||
<label htmlFor="icon-button-file">
|
||||
<IconButton component="span">
|
||||
<Upload />
|
||||
</IconButton>
|
||||
</label>
|
||||
<IconButton type="submit">
|
||||
<SaveAs />
|
||||
</IconButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecordMp3;
|
||||
37
client/src/components/RecoverForm.tsx
Normal file
37
client/src/components/RecoverForm.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import Form from "./Form";
|
||||
import { FormSubmit, FormTextField } from "./StyledElements";
|
||||
|
||||
interface RecoverFormProps {
|
||||
form: {
|
||||
email: string;
|
||||
};
|
||||
error: string;
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
function RecoverForm({
|
||||
form,
|
||||
error,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
}: RecoverFormProps) {
|
||||
return (
|
||||
<Form text="Recover Password" error={error}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormTextField
|
||||
required
|
||||
label="Email"
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={form.email}
|
||||
/>
|
||||
<FormSubmit color="primary" type="submit">
|
||||
Recover my password
|
||||
</FormSubmit>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecoverForm;
|
||||
90
client/src/components/RegisterForm.tsx
Normal file
90
client/src/components/RegisterForm.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Typography } from "@mui/material";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import Form from "./Form";
|
||||
import { FormSubmit, FormTextField } from "./StyledElements";
|
||||
|
||||
interface RegisterFormProps {
|
||||
form: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
pronouns: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
error: string;
|
||||
}
|
||||
|
||||
function RegisterForm({
|
||||
form,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
error,
|
||||
}: RegisterFormProps) {
|
||||
return (
|
||||
<Form text="Create an Account" error={error}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormTextField
|
||||
required
|
||||
label="First Name"
|
||||
name="firstName"
|
||||
onChange={handleChange}
|
||||
value={form.firstName}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Last Name"
|
||||
name="lastName"
|
||||
onChange={handleChange}
|
||||
value={form.lastName}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Email"
|
||||
name="email"
|
||||
onChange={handleChange}
|
||||
value={form.email}
|
||||
/>
|
||||
<FormTextField
|
||||
label="Pronouns"
|
||||
name="pronouns"
|
||||
onChange={handleChange}
|
||||
value={form.pronouns}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Password"
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
value={form.password}
|
||||
other={{ type: "password" }}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
onChange={handleChange}
|
||||
value={form.confirmPassword}
|
||||
other={{ type: "password" }}
|
||||
/>
|
||||
<FormSubmit
|
||||
color="primary"
|
||||
type="submit"
|
||||
other={{ disabled: form.password !== form.confirmPassword }}
|
||||
>
|
||||
Create an Account
|
||||
</FormSubmit>
|
||||
<Typography variant="body1">
|
||||
Already have an account?
|
||||
{" "}
|
||||
<Link to="/login">Login</Link>
|
||||
</Typography>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default RegisterForm;
|
||||
51
client/src/components/ResetForm.tsx
Normal file
51
client/src/components/ResetForm.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import Form from "./Form";
|
||||
import { FormSubmit, FormTextField } from "./StyledElements";
|
||||
|
||||
interface ResetFormProps {
|
||||
form: {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
error: string;
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
}
|
||||
|
||||
function ResetForm({
|
||||
form,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
error,
|
||||
}: ResetFormProps) {
|
||||
return (
|
||||
<Form text="Reset Password" error={error}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormTextField
|
||||
required
|
||||
label="Password"
|
||||
name="password"
|
||||
onChange={handleChange}
|
||||
value={form.password}
|
||||
other={{ type: "password" }}
|
||||
/>
|
||||
<FormTextField
|
||||
required
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
onChange={handleChange}
|
||||
value={form.confirmPassword}
|
||||
other={{ type: "password" }}
|
||||
/>
|
||||
<FormSubmit
|
||||
color="primary"
|
||||
type="submit"
|
||||
other={{ disabled: form.password !== form.confirmPassword }}
|
||||
>
|
||||
Reset my Password
|
||||
</FormSubmit>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResetForm;
|
||||
26
client/src/components/StackItem.tsx
Normal file
26
client/src/components/StackItem.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { Paper, Typography } from "@mui/material";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
interface StackItemProps {
|
||||
text?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function StackItem({ text, children }: StackItemProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const paperStyle = {
|
||||
...theme.typography.body2,
|
||||
padding: "2vh",
|
||||
color: theme.palette.text.secondary,
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={paperStyle}>
|
||||
<Typography variant="h6">{text}</Typography>
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default StackItem;
|
||||
119
client/src/components/StyledElements.tsx
Normal file
119
client/src/components/StyledElements.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
TextFieldProps,
|
||||
ButtonProps,
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
const Form = styled(FormGroup)(({ theme }) => ({
|
||||
[theme.breakpoints.only("xs")]: {
|
||||
column: true,
|
||||
},
|
||||
}));
|
||||
const FormField = styled(TextField)(({ theme }) => ({
|
||||
[theme.breakpoints.only("xs")]: {
|
||||
column: true,
|
||||
width: "75%",
|
||||
},
|
||||
}));
|
||||
const FormButton = styled(Button)(({ theme }) => ({
|
||||
disableElevation: true,
|
||||
color: theme.palette.primary.main,
|
||||
[theme.breakpoints.only("xs")]: {
|
||||
column: true,
|
||||
width: "25%",
|
||||
},
|
||||
}));
|
||||
|
||||
const DynamicStack = styled(Stack)(({ theme }) => ({
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
spacing: 2,
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
}));
|
||||
|
||||
const DynamicPaper = styled(Paper)(({ theme }) => ({
|
||||
...theme.typography.body2,
|
||||
padding: "2vh",
|
||||
textAlign: "center",
|
||||
color: theme.palette.text.secondary,
|
||||
flexGrow: 1,
|
||||
margin: "0 2vh",
|
||||
[theme.breakpoints.only("xs")]: {
|
||||
height: "80vh",
|
||||
},
|
||||
[theme.breakpoints.only("sm")]: {
|
||||
height: "80vh",
|
||||
},
|
||||
[theme.breakpoints.only("md")]: {
|
||||
height: "82vh",
|
||||
},
|
||||
}));
|
||||
|
||||
interface FormTextFieldProps {
|
||||
label: string;
|
||||
name: string;
|
||||
value: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
required?: boolean;
|
||||
other?: TextFieldProps;
|
||||
}
|
||||
|
||||
function FormTextField({
|
||||
label,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
required,
|
||||
other,
|
||||
}: FormTextFieldProps) {
|
||||
return (
|
||||
<TextField
|
||||
required={required}
|
||||
fullWidth
|
||||
label={label}
|
||||
name={name}
|
||||
variant="outlined"
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
sx={{ margin: "1vh 0vh" }}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormSubmitProps {
|
||||
color: "primary" | "secondary" | "error";
|
||||
children: React.ReactNode;
|
||||
type: "button" | "submit";
|
||||
other?: ButtonProps;
|
||||
}
|
||||
|
||||
function FormSubmit({ color, children, type, other }: FormSubmitProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
color={color}
|
||||
sx={{ margin: "1vh 0vh" }}
|
||||
type={type}
|
||||
fullWidth
|
||||
{...other}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DynamicPaper,
|
||||
DynamicStack,
|
||||
Form,
|
||||
FormButton,
|
||||
FormField,
|
||||
FormSubmit,
|
||||
FormTextField,
|
||||
};
|
||||
31
client/src/components/UpdateOrganizationForm.tsx
Normal file
31
client/src/components/UpdateOrganizationForm.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import Form from "./Form";
|
||||
import { FormSubmit, FormTextField } from "./StyledElements";
|
||||
|
||||
interface UpdateOrganizationFormProps {
|
||||
name: string;
|
||||
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
||||
}
|
||||
|
||||
export default function UpdateOrganizationForm({
|
||||
name,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
}: UpdateOrganizationFormProps) {
|
||||
return (
|
||||
<Form text="Rename Organization">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormTextField
|
||||
required
|
||||
label="Name"
|
||||
name="name"
|
||||
onChange={handleChange}
|
||||
value={name}
|
||||
/>
|
||||
<FormSubmit color="primary" type="submit">
|
||||
Rename
|
||||
</FormSubmit>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,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('/login');
|
||||
})
|
||||
.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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
27
client/src/components/index.ts
Normal file
27
client/src/components/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Navbar from "./Navbar";
|
||||
import MobileNavbar from "./MobileNavbar";
|
||||
import MainPage from "./MainPage";
|
||||
import LoginForm from "./LoginForm";
|
||||
import RegisterForm from "./RegisterForm";
|
||||
import EditUserForm from "./EditUserForm";
|
||||
import RecordMp3 from "./RecordMp3";
|
||||
import RecoverForm from "./RecoverForm";
|
||||
import ResetForm from "./ResetForm";
|
||||
import OrganizationList from "./OrganizationList";
|
||||
import Dashboard from "./Dashboard";
|
||||
import UpdateOrganizationForm from "./UpdateOrganizationForm";
|
||||
|
||||
export {
|
||||
Navbar,
|
||||
MobileNavbar,
|
||||
MainPage,
|
||||
LoginForm,
|
||||
RegisterForm,
|
||||
EditUserForm,
|
||||
RecordMp3,
|
||||
RecoverForm,
|
||||
ResetForm,
|
||||
OrganizationList,
|
||||
Dashboard,
|
||||
UpdateOrganizationForm,
|
||||
};
|
||||
|
|
@ -1,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;
|
||||
|
|
@ -1,245 +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,
|
||||
} from '@mui/material';
|
||||
|
||||
import Dashboard from '../utils/Dashboard';
|
||||
|
||||
function OrgDashboard() {
|
||||
const { orgID } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [org, setOrg] = useState({
|
||||
orgID: '',
|
||||
orgName: '',
|
||||
orgOwner: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
},
|
||||
orgMembers: [],
|
||||
createdAt: '',
|
||||
memberCount: 0,
|
||||
});
|
||||
const [rows, setRows] = useState([]);
|
||||
const [status, setStatus] = useState('');
|
||||
const [selection, setSelection] = useState();
|
||||
|
||||
const linkStyle = { textDecoration: 'none', color: 'inherit' };
|
||||
|
||||
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('/');
|
||||
});
|
||||
}
|
||||
|
||||
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}>
|
||||
<Typography variant='h6'>Organizations/{org.orgName}</Typography>
|
||||
<Box sx={{ marginTop: '1vh' }}>
|
||||
<Grid container columns={{ xs: 3, md: 12 }}>
|
||||
<Grid item xs={3}>
|
||||
<Stack spacing={2}>
|
||||
<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>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='body1'>
|
||||
Owner:{' '}
|
||||
{`${org.orgOwner.firstName} ${org.orgOwner.lastName}`}
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
Email: {org.orgOwner.email}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
direction='row'
|
||||
justifyContent='flex-end'
|
||||
alignItems='flex-end'
|
||||
xs
|
||||
>
|
||||
<Stack spacing={2} alignItems='flex-end'>
|
||||
{status ? (
|
||||
<React.Fragment>
|
||||
<ButtonGroup
|
||||
variant='outlined'
|
||||
aria-label='outlined button group'
|
||||
>
|
||||
<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'
|
||||
aria-label='outlined button group'
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Dashboard>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrgDashboard;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Paper, Box, IconButton } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
|
||||
function Dashboard({ children, rows, setSelection, onClick }) {
|
||||
const theme = useTheme();
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Paper
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
padding: '2vh',
|
||||
color: theme.palette.text.secondary,
|
||||
flexGrow: 1,
|
||||
height: '20vh',
|
||||
margin: '2vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Outlet />
|
||||
</Paper>
|
||||
<Paper
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
padding: '2vh',
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text.secondary,
|
||||
flexGrow: 1,
|
||||
height: '67vh',
|
||||
margin: '2vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ height: '63vh' }}>
|
||||
<DataGrid
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
pageSize={5}
|
||||
rowsPerPageOptions={[5]}
|
||||
onSelectionModelChange={(newSelection) => {
|
||||
setSelection(newSelection.rows);
|
||||
}}
|
||||
checkboxSelection
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
|
@ -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={1} sm={3} md={4} />
|
||||
<Grid item xs={10} sm={6} md={4}>
|
||||
<Paper
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
padding: '2vh',
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Outlet />
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={1} sm={3} md={4} />
|
||||
</Grid>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default Form;
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Stack, Box, Typography } from '@mui/material';
|
||||
|
||||
function OrgUnit({ org }) {
|
||||
const { orgID, orgName, memberCount, createdAt } = org;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography variant='h6'>
|
||||
<Link to={`org/${orgID}`}>Name: {orgName}</Link>
|
||||
</Typography>
|
||||
<Box>
|
||||
<Stack spacing={2} direction={{ xs: 'column', md: 'row' }}>
|
||||
<Typography variant='body1'>
|
||||
Members: {memberCount}
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
Created On: {createdAt}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrgUnit;
|
||||
|
|
@ -1,38 +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([]);
|
||||
|
||||
useEffect(() => {
|
||||
retrieveOrgs(setOrgs);
|
||||
}, [retrieveOrgs]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
...theme.typography.body2,
|
||||
padding: '2vh',
|
||||
color: theme.palette.text.secondary,
|
||||
flexGrow: 1,
|
||||
height: '89vh',
|
||||
margin: '2vh',
|
||||
}}
|
||||
>
|
||||
<Typography variant='h6'>Organizations</Typography>
|
||||
<Box sx={{ overflowY: 'auto' }}>
|
||||
{orgs.map((org) => (
|
||||
<OrgUnit org={org} />
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Organizations;
|
||||
|
|
@ -1,134 +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('token')}.mp3`,
|
||||
{
|
||||
responseType: 'blob',
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
setUploadedFile(() => {
|
||||
return new File([res.data], 'userAudio.mp3', {
|
||||
type: res.data.type,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
function togglePlay() {
|
||||
console.log(uploadedFile);
|
||||
if (audio && !audio.ended) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
setAudio(() => null);
|
||||
} else {
|
||||
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
|
||||
setAudio(() => audioFile);
|
||||
audioFile.play();
|
||||
}
|
||||
}
|
||||
|
||||
async function record() {
|
||||
if (recording) {
|
||||
console.log('Stopped Recording');
|
||||
recorder
|
||||
.stop()
|
||||
.getMp3()
|
||||
.then(([buffer, blob]) => {
|
||||
setRecording(() => false);
|
||||
setUploadedFile(() => {
|
||||
return new File(buffer, 'userAudio.mp3', {
|
||||
type: blob.type,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
recorder
|
||||
.start()
|
||||
.then(() => {
|
||||
setRecording(() => true);
|
||||
})
|
||||
.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;
|
||||
|
|
@ -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;
|
||||
16
client/src/containers/EditUser.tsx
Normal file
16
client/src/containers/EditUser.tsx
Normal 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;
|
||||
49
client/src/containers/Layout.tsx
Normal file
49
client/src/containers/Layout.tsx
Normal 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;
|
||||
16
client/src/containers/Login.tsx
Normal file
16
client/src/containers/Login.tsx
Normal 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;
|
||||
14
client/src/containers/MainPage.tsx
Normal file
14
client/src/containers/MainPage.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import Helmet from "react-helmet";
|
||||
|
||||
import { MainPage as MainPageController } from "../controllers";
|
||||
|
||||
export default function MainPage() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>KnowItAll</title>
|
||||
</Helmet>
|
||||
<MainPageController />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
client/src/containers/MemberOrgs.tsx
Normal file
14
client/src/containers/MemberOrgs.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
client/src/containers/OrgDashboard.tsx
Normal file
14
client/src/containers/OrgDashboard.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
16
client/src/containers/OwnedOrgs.tsx
Normal file
16
client/src/containers/OwnedOrgs.tsx
Normal 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;
|
||||
16
client/src/containers/Recover.tsx
Normal file
16
client/src/containers/Recover.tsx
Normal 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;
|
||||
16
client/src/containers/Register.tsx
Normal file
16
client/src/containers/Register.tsx
Normal 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;
|
||||
14
client/src/containers/Reset.tsx
Normal file
14
client/src/containers/Reset.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
14
client/src/containers/UpdateOrganization.tsx
Normal file
14
client/src/containers/UpdateOrganization.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
client/src/containers/index.ts
Normal file
25
client/src/containers/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Layout from "./Layout";
|
||||
import MainPage from "./MainPage";
|
||||
import Login from "./Login";
|
||||
import Register from "./Register";
|
||||
import EditUser from "./EditUser";
|
||||
import Recover from "./Recover";
|
||||
import Reset from "./Reset";
|
||||
import OwnedOrgs from "./OwnedOrgs";
|
||||
import MemberOrgs from "./MemberOrgs";
|
||||
import OrgDashboard from "./OrgDashboard";
|
||||
import UpdateOrganization from "./UpdateOrganization";
|
||||
|
||||
export {
|
||||
Layout,
|
||||
MainPage,
|
||||
Login,
|
||||
Register,
|
||||
EditUser,
|
||||
Recover,
|
||||
Reset,
|
||||
OwnedOrgs,
|
||||
MemberOrgs,
|
||||
OrgDashboard,
|
||||
UpdateOrganization,
|
||||
};
|
||||
19
client/src/contexts/ContextProvider.tsx
Normal file
19
client/src/contexts/ContextProvider.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
import ErrorProvider from "./ErrorProvider";
|
||||
import ModeProvider from "./ModeProvider";
|
||||
import UserProvider from "./UserProvider";
|
||||
|
||||
interface ContextProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function ContextProvider({ children }: ContextProviderProps) {
|
||||
return (
|
||||
<ModeProvider>
|
||||
<ErrorProvider>
|
||||
<UserProvider>{children}</UserProvider>
|
||||
</ErrorProvider>
|
||||
</ModeProvider>
|
||||
);
|
||||
}
|
||||
20
client/src/contexts/ErrorContext.ts
Normal file
20
client/src/contexts/ErrorContext.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import React, { createContext, useContext } from "react";
|
||||
|
||||
interface ErrorContextData {
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const ErrorContext = createContext<ErrorContextData | undefined>(undefined);
|
||||
|
||||
export default ErrorContext;
|
||||
|
||||
export const useError = () => {
|
||||
const context = useContext(ErrorContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useError must be used within a ErrorProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
22
client/src/contexts/ErrorProvider.tsx
Normal file
22
client/src/contexts/ErrorProvider.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useMemo, useState } from "react";
|
||||
|
||||
import ErrorContext from "./ErrorContext";
|
||||
|
||||
interface ErrorProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ErrorProvider({ children }: ErrorProviderProps) {
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ error, setError }),
|
||||
[error, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ErrorContext.Provider>
|
||||
);
|
||||
}
|
||||
22
client/src/contexts/ModeContext.ts
Normal file
22
client/src/contexts/ModeContext.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import React, { createContext, useContext } from "react";
|
||||
|
||||
import { ThemeMode } from "../types";
|
||||
|
||||
interface ModeContextData {
|
||||
mode: ThemeMode;
|
||||
setMode: React.Dispatch<React.SetStateAction<ThemeMode>>;
|
||||
}
|
||||
|
||||
const ModeContext = createContext<ModeContextData | undefined>(undefined);
|
||||
|
||||
export default ModeContext;
|
||||
|
||||
export const useMode = () => {
|
||||
const context = useContext(ModeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useMode must be used within a ModeProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
27
client/src/contexts/ModeProvider.tsx
Normal file
27
client/src/contexts/ModeProvider.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { ThemeMode } from "../types";
|
||||
import ModeContext from "./ModeContext";
|
||||
|
||||
interface ModeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ModeProvider({ children }: ModeProviderProps) {
|
||||
const [mode, setMode] = useState<ThemeMode>(
|
||||
(Cookies.get("theme") as ThemeMode) || "light"
|
||||
);
|
||||
|
||||
const contextValue = useMemo(() => ({ mode, setMode }), [mode, setMode]);
|
||||
|
||||
useEffect(() => {
|
||||
Cookies.set("theme", mode);
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<ModeContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ModeContext.Provider>
|
||||
);
|
||||
}
|
||||
23
client/src/contexts/UserContext.ts
Normal file
23
client/src/contexts/UserContext.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import React, { createContext, useContext } from "react";
|
||||
|
||||
import { User } from "../types";
|
||||
|
||||
interface UserContextData {
|
||||
loading: boolean;
|
||||
userData: User;
|
||||
setUserData: React.Dispatch<React.SetStateAction<User>>;
|
||||
}
|
||||
|
||||
const UserContext = createContext<UserContextData | undefined>(undefined);
|
||||
|
||||
export default UserContext;
|
||||
|
||||
export const useUser = () => {
|
||||
const context = useContext(UserContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useUser must be used within a UserProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
63
client/src/contexts/UserProvider.tsx
Normal file
63
client/src/contexts/UserProvider.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import Cookies from "js-cookie";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import UserContext from "./UserContext";
|
||||
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
import { User } from "../types";
|
||||
|
||||
interface UserProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function UserProvider({ children }: UserProviderProps) {
|
||||
const [userData, setUserData] = useState<User>({
|
||||
id: -1,
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
pronouns: "",
|
||||
nickname: "",
|
||||
username: "",
|
||||
email: "",
|
||||
age: 0,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({ loading, userData, setUserData }),
|
||||
[loading, userData, setUserData]
|
||||
);
|
||||
|
||||
const loadUserData = useCallback(async () => {
|
||||
if (!loading) return;
|
||||
|
||||
if (!Cookies.get("token")) {
|
||||
setLoading(() => false);
|
||||
}
|
||||
|
||||
try {
|
||||
const instance = createRequest();
|
||||
const response = await instance.post("/auth/remember");
|
||||
|
||||
setUserData(() => response.data.user);
|
||||
} catch (error) {
|
||||
Cookies.remove("token");
|
||||
navigate("/login");
|
||||
} finally {
|
||||
setLoading(() => false);
|
||||
}
|
||||
}, [navigate, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
loadUserData();
|
||||
}, [loadUserData]);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
}
|
||||
8
client/src/contexts/index.ts
Normal file
8
client/src/contexts/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { useMode } from "./ModeContext";
|
||||
import { useUser } from "./UserContext";
|
||||
import { useError } from "./ErrorContext";
|
||||
import ContextProvider from "./ContextProvider";
|
||||
|
||||
export {
|
||||
useMode, useUser, useError, ContextProvider,
|
||||
};
|
||||
113
client/src/controllers/Dashboard.tsx
Normal file
113
client/src/controllers/Dashboard.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import axios from "axios";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { Dashboard as DashboardComponent } from "../components";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { orgID } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [org, setOrg] = useState({
|
||||
id: orgID,
|
||||
name: "",
|
||||
owner: {
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
},
|
||||
member: [],
|
||||
createdAt: "",
|
||||
memberCount: 0,
|
||||
});
|
||||
const [rows, setRows] = useState([]);
|
||||
const [status, setStatus] = useState(true);
|
||||
const [selection, setSelection] = useState(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const getOrg = useCallback(async () => {
|
||||
try {
|
||||
const instance = createRequest();
|
||||
const response = await instance.get(`/org/${orgID}`);
|
||||
|
||||
const orgData = {
|
||||
...response.data.org,
|
||||
memberCount: response.data.memberCount,
|
||||
};
|
||||
|
||||
setOrg(() => orgData);
|
||||
setRows(() => response.data.org.member);
|
||||
setStatus(() => response.data.owner);
|
||||
} catch (error) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, orgID]);
|
||||
|
||||
useEffect(() => {
|
||||
getOrg();
|
||||
}, [getOrg]);
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
setOpen((initial) => !initial);
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
const instance = createRequest();
|
||||
await instance.delete(`/org/${orgID}`);
|
||||
navigate("/");
|
||||
}, [navigate, orgID]);
|
||||
|
||||
const copyID = useCallback(() => {
|
||||
navigator.clipboard.writeText(orgID);
|
||||
}, [orgID]);
|
||||
|
||||
const copyJoinLink = useCallback(() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${process.env.REACT_APP_DOMAIN_ROOT}/register?orgID=${orgID}`
|
||||
);
|
||||
}, [orgID]);
|
||||
|
||||
const removeSelected = useCallback(async () => {
|
||||
const doomedUserIDs = selection.map((row) => row.id);
|
||||
|
||||
const instance = createRequest();
|
||||
await instance.post(`/org/${orgID}/remove`, { doomedUserIDs });
|
||||
}, [orgID, selection]);
|
||||
|
||||
const leaveOrg = useCallback(async () => {
|
||||
const instance = createRequest();
|
||||
await instance.post(`/org/${orgID}/remove`, {});
|
||||
navigate("/");
|
||||
}, [navigate, orgID]);
|
||||
|
||||
const play = useCallback(async (id) => {
|
||||
const request = await axios.get(
|
||||
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${id}.mp3`,
|
||||
{ responseType: "blob" }
|
||||
);
|
||||
const uploadedFile = new File([request.data], "userAudio.mp3", {
|
||||
type: request.data.type,
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
|
||||
audioFile.play();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DashboardComponent
|
||||
org={org}
|
||||
rows={rows}
|
||||
open={open}
|
||||
handleOpen={handleOpen}
|
||||
setSelection={setSelection}
|
||||
onClick={play}
|
||||
status={status}
|
||||
handleDelete={handleDelete}
|
||||
copyID={copyID}
|
||||
copyJoinLink={copyJoinLink}
|
||||
removeSelected={removeSelected}
|
||||
leaveOrg={leaveOrg}
|
||||
/>
|
||||
);
|
||||
}
|
||||
67
client/src/controllers/EditUserForm.tsx
Normal file
67
client/src/controllers/EditUserForm.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { EditUserForm } from "../components";
|
||||
import { useUser, useError } from "../contexts";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
function EditUserFormController() {
|
||||
const navigate = useNavigate();
|
||||
const { userData } = useUser();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
pronouns: "",
|
||||
});
|
||||
|
||||
const { error } = useError();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name } = e.target;
|
||||
const { value } = e.target;
|
||||
|
||||
setForm((form) => ({ ...form, [name]: value }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const instance = createRequest();
|
||||
await instance.post("/auth/update", form);
|
||||
navigate("/");
|
||||
} catch (e) {
|
||||
const error = e as AxiosError;
|
||||
if (error.response?.status === 401) navigate("/login");
|
||||
}
|
||||
},
|
||||
[form, navigate]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setForm(() => ({
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
email: userData.email,
|
||||
pronouns: userData.pronouns,
|
||||
}));
|
||||
}, [userData]);
|
||||
|
||||
return (
|
||||
<EditUserForm
|
||||
form={form}
|
||||
error={error}
|
||||
handleSubmit={handleSubmit}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditUserFormController;
|
||||
89
client/src/controllers/LoginForm.tsx
Normal file
89
client/src/controllers/LoginForm.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { Buffer } from "buffer";
|
||||
import Cookies from "js-cookie";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { LoginForm } from "../components";
|
||||
import { useError, useUser } from "../contexts";
|
||||
import createRequest from "../utils/requests";
|
||||
import { APIError } from "../types";
|
||||
|
||||
function LoginFormController() {
|
||||
const navigate = useNavigate();
|
||||
const { error, setError } = useError();
|
||||
const { setUserData } = useUser();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name } = e.target;
|
||||
const { value } = e.target;
|
||||
|
||||
setForm((prevForm) => ({ ...prevForm, [name]: value }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(e: React.SyntheticEvent<Element, Event>) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
const { name } = target;
|
||||
const { checked } = target;
|
||||
|
||||
setForm((prevForm) => ({ ...prevForm, [name]: checked }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const encoded = Buffer.from(
|
||||
`${form.email}:${form.password}`
|
||||
).toString("base64");
|
||||
|
||||
const instance = createRequest();
|
||||
instance.defaults.headers.common.Authorization = `basic ${encoded}`;
|
||||
|
||||
try {
|
||||
const response = await instance.post("/auth/login", {
|
||||
remember: form.remember,
|
||||
});
|
||||
const expires = form.remember ? 365 : 1;
|
||||
|
||||
setUserData(() => response.data.user);
|
||||
Cookies.set("token", response.data.token, { expires });
|
||||
|
||||
navigate("/");
|
||||
} catch (e) {
|
||||
const error = e as APIError;
|
||||
|
||||
if (error.response?.data?.error) {
|
||||
setError(error.response.data.error);
|
||||
} else {
|
||||
setError("An error occurred");
|
||||
}
|
||||
}
|
||||
},
|
||||
[form, navigate, setUserData, setError]
|
||||
);
|
||||
|
||||
return (
|
||||
<LoginForm
|
||||
form={form}
|
||||
handleSubmit={handleSubmit}
|
||||
handleChange={handleChange}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginFormController;
|
||||
70
client/src/controllers/MainPage.tsx
Normal file
70
client/src/controllers/MainPage.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { MainPage as MainPageComponent } from "../components";
|
||||
import createRequest from "../utils/requests";
|
||||
import { Organization } from "../types";
|
||||
|
||||
export default function MainPage() {
|
||||
const [ownedOrgs, setOwnedOrgs] = useState<Organization[]>([]);
|
||||
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
id: "",
|
||||
});
|
||||
|
||||
const getOwnedOrgs = useCallback(async () => {
|
||||
const instance = createRequest();
|
||||
const response = await instance.get("/auth/orgs");
|
||||
setOwnedOrgs(() => response.data.orgs);
|
||||
}, []);
|
||||
|
||||
const getMemberOrgs = useCallback(async () => {
|
||||
const instance = createRequest();
|
||||
const response = await instance.get("/auth/orgs/member");
|
||||
setOrgs(() => response.data.orgs);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getOwnedOrgs();
|
||||
getMemberOrgs();
|
||||
}, [getOwnedOrgs, getMemberOrgs]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name } = e.target;
|
||||
const { value } = e.target;
|
||||
|
||||
setForm((form) => ({ ...form, [name]: value }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const makeOrg = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const instance = createRequest();
|
||||
instance.post("/org/create", form).then(getOwnedOrgs);
|
||||
},
|
||||
[form, getOwnedOrgs]
|
||||
);
|
||||
|
||||
const joinOrg = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const instance = createRequest();
|
||||
instance.post(`/org/${form.id}/add`, {}).then(getMemberOrgs);
|
||||
},
|
||||
[form, getMemberOrgs]
|
||||
);
|
||||
|
||||
return (
|
||||
<MainPageComponent
|
||||
form={form}
|
||||
orgs={orgs}
|
||||
ownedOrgs={ownedOrgs}
|
||||
handleChange={handleChange}
|
||||
joinOrg={joinOrg}
|
||||
createOrg={makeOrg}
|
||||
/>
|
||||
);
|
||||
}
|
||||
21
client/src/controllers/MemberOrgsDashboard.tsx
Normal file
21
client/src/controllers/MemberOrgsDashboard.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { OrganizationList } from "../components";
|
||||
import createRequest from "../utils/requests";
|
||||
import { Organization } from "../types";
|
||||
|
||||
export default function MemberOrgsDashboard() {
|
||||
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||
|
||||
const getOrgs = useCallback(async () => {
|
||||
const instance = createRequest();
|
||||
const response = await instance.get("/auth/orgs");
|
||||
setOrgs(() => response.data.orgs);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getOrgs();
|
||||
}, [getOrgs]);
|
||||
|
||||
return <OrganizationList orgs={orgs} />;
|
||||
}
|
||||
61
client/src/controllers/Navbar.tsx
Normal file
61
client/src/controllers/Navbar.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { useMediaQuery, Theme } from "@mui/material";
|
||||
import Cookies from "js-cookie";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { MobileNavbar, Navbar } from "../components";
|
||||
import { useMode, useUser } from "../contexts";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
export default function NavbarController() {
|
||||
const navigate = useNavigate();
|
||||
const { userData, setUserData } = useUser();
|
||||
const { mode, setMode } = useMode();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const toggleOpen = () => setOpen((initial) => !initial);
|
||||
|
||||
const toggleMode = () => {
|
||||
setMode(() => (mode === "light" ? "dark" : "light"));
|
||||
};
|
||||
|
||||
const sm = useMediaQuery((theme: Theme) => theme.breakpoints.only("sm"));
|
||||
const md = useMediaQuery((theme: Theme) => theme.breakpoints.only("md"));
|
||||
|
||||
const logoutUser = async () => {
|
||||
try {
|
||||
const instance = createRequest();
|
||||
await instance.post("/auth/logout");
|
||||
} finally {
|
||||
Cookies.remove("token");
|
||||
setUserData({
|
||||
email: "",
|
||||
firstName: "",
|
||||
username: "",
|
||||
age: 0,
|
||||
lastName: "",
|
||||
pronouns: "",
|
||||
nickname: "",
|
||||
id: 0,
|
||||
});
|
||||
navigate("/login");
|
||||
}
|
||||
};
|
||||
|
||||
return md || sm
|
||||
? Navbar({
|
||||
logout: logoutUser,
|
||||
userData,
|
||||
mode,
|
||||
toggleMode,
|
||||
})
|
||||
: MobileNavbar({
|
||||
logout: logoutUser,
|
||||
userData,
|
||||
open,
|
||||
toggleOpen,
|
||||
mode,
|
||||
toggleMode,
|
||||
});
|
||||
}
|
||||
21
client/src/controllers/OwnedOrgsDashboard.tsx
Normal file
21
client/src/controllers/OwnedOrgsDashboard.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { OrganizationList } from "../components";
|
||||
import { Organization } from "../types";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
export default function OwnedOrgsDashboard() {
|
||||
const [orgs, setOrgs] = useState<Organization[]>([]);
|
||||
|
||||
const getOrgs = useCallback(async () => {
|
||||
const instance = createRequest();
|
||||
const response = await instance.get("/auth/orgs/member");
|
||||
setOrgs(() => response.data.orgs);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getOrgs();
|
||||
}, [getOrgs]);
|
||||
|
||||
return <OrganizationList orgs={orgs} />;
|
||||
}
|
||||
103
client/src/controllers/RecordMp3.jsx
Normal file
103
client/src/controllers/RecordMp3.jsx
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import axios from "axios";
|
||||
import MicRecorder from "mic-recorder-to-mp3";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { RecordMp3 as RecordMp3Component } from "../components";
|
||||
import { useUser } from "../contexts";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
export default function RecordMp3() {
|
||||
const { userData } = useUser();
|
||||
|
||||
const [recorder] = useState(
|
||||
new MicRecorder({
|
||||
bitRate: 128,
|
||||
})
|
||||
);
|
||||
const [recording, setRecording] = useState(false);
|
||||
const [uploadedFile, setUploadedFile] = useState();
|
||||
const [audioRecording, setAudioRecording] = useState(null);
|
||||
|
||||
const getData = useCallback(async () => {
|
||||
const response = await axios.get(
|
||||
`${process.env.REACT_APP_DOMAIN_ROOT}/public/audio/${userData.id}.mp3`,
|
||||
{ responseType: "blob" }
|
||||
);
|
||||
setUploadedFile(
|
||||
() =>
|
||||
new File([response.data], "userAudio.mp3", {
|
||||
type: response.data.type,
|
||||
lastModified: Date.now(),
|
||||
})
|
||||
);
|
||||
}, [userData.id]);
|
||||
|
||||
useEffect(() => {
|
||||
getData();
|
||||
}, [getData]);
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
if (audioRecording && !audioRecording.ended) {
|
||||
audioRecording.pause();
|
||||
audioRecording.currentTime = 0;
|
||||
setAudioRecording(() => null);
|
||||
} else {
|
||||
const audioFile = new Audio(URL.createObjectURL(uploadedFile));
|
||||
setAudioRecording(() => audioFile);
|
||||
audioFile.play();
|
||||
}
|
||||
}, [audioRecording, uploadedFile]);
|
||||
|
||||
const record = useCallback(() => {
|
||||
if (recording) {
|
||||
setRecording(() => false);
|
||||
recorder
|
||||
.stop()
|
||||
.getMp3()
|
||||
.then(([buffer, blob]) => {
|
||||
setUploadedFile(
|
||||
() =>
|
||||
new File(buffer, "userAudio.mp3", {
|
||||
type: blob.type,
|
||||
lastModified: Date.now(),
|
||||
})
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setRecording(() => true);
|
||||
recorder.start();
|
||||
}
|
||||
}, [recorder, recording]);
|
||||
|
||||
const uploadRecord = useCallback(() => {
|
||||
setUploadedFile(
|
||||
() => document.getElementById("icon-button-file").files[0]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("audioFile", uploadedFile, "userAudio.mp3");
|
||||
|
||||
const instance = createRequest();
|
||||
instance.defaults.headers.common["Content-Type"] =
|
||||
"multipart/form-data";
|
||||
|
||||
return instance.post("/auth/audio", formData);
|
||||
},
|
||||
[uploadedFile]
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordMp3Component
|
||||
togglePlay={togglePlay}
|
||||
record={record}
|
||||
uploadRecord={uploadRecord}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
52
client/src/controllers/RecoverForm.tsx
Normal file
52
client/src/controllers/RecoverForm.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { RecoverForm } from "../components";
|
||||
import createRequest from "../utils/requests";
|
||||
import { APIError } from "../types";
|
||||
|
||||
function RecoverFormController() {
|
||||
const [form, setForm] = useState({
|
||||
email: "",
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name } = e.target;
|
||||
const { value } = e.target;
|
||||
|
||||
setForm((form) => ({ ...form, [name]: value }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const instance = createRequest();
|
||||
await instance.post("/auth/reset", form);
|
||||
} catch (e) {
|
||||
const error = e as APIError;
|
||||
if (error.response.data.error) {
|
||||
setError(error.response.data.error);
|
||||
} else {
|
||||
setError("An error occurred");
|
||||
}
|
||||
}
|
||||
},
|
||||
[form]
|
||||
);
|
||||
|
||||
return (
|
||||
<RecoverForm
|
||||
form={form}
|
||||
error={error}
|
||||
handleChange={handleChange}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecoverFormController;
|
||||
65
client/src/controllers/RegisterForm.tsx
Normal file
65
client/src/controllers/RegisterForm.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { RegisterForm } from "../components";
|
||||
import { useError } from "../contexts";
|
||||
import createRequest from "../utils/requests";
|
||||
import { APIError } from "../types";
|
||||
|
||||
function RegisterFormController() {
|
||||
const navigate = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
const { error, setError } = useError();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
pronouns: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { name } = e.target;
|
||||
const { value } = e.target;
|
||||
|
||||
setForm((form) => ({ ...form, [name]: value }));
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { confirmPassword, ...filteredForm } = form;
|
||||
|
||||
const fullForm = { ...filteredForm, orgID: params.get("orgID") };
|
||||
|
||||
try {
|
||||
const instance = createRequest();
|
||||
await instance.post("/auth/register", fullForm);
|
||||
navigate("/login");
|
||||
} catch (e) {
|
||||
const error = e as APIError;
|
||||
|
||||
if (error.response.data.error) {
|
||||
setError(error.response.data.error);
|
||||
} else {
|
||||
setError("An error occurred");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RegisterForm
|
||||
{...{
|
||||
form,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
error,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default RegisterFormController;
|
||||
47
client/src/controllers/ResetForm.tsx
Normal file
47
client/src/controllers/ResetForm.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { ResetForm as ResetFormComponent } from "../components";
|
||||
import { useError } from "../contexts";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
export default function ResetForm() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const { error } = useError();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name } = e.target;
|
||||
const { value } = e.target;
|
||||
|
||||
setForm((form) => ({ ...form, [name]: value }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const instance = createRequest();
|
||||
await instance.patch(`/reset/${id}`);
|
||||
navigate("/login");
|
||||
},
|
||||
[id, navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<ResetFormComponent
|
||||
form={form}
|
||||
handleChange={handleChange}
|
||||
handleSubmit={handleSubmit}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
}
|
||||
39
client/src/controllers/UpdateOrganizationForm.tsx
Normal file
39
client/src/controllers/UpdateOrganizationForm.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { UpdateOrganizationForm as UpdateOrganizationFormComponent } from "../components";
|
||||
import createRequest from "../utils/requests";
|
||||
|
||||
export default function UpdateOrganizationForm() {
|
||||
const { orgID } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
|
||||
setName(() => value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const instance = createRequest();
|
||||
instance.patch(`/org/${orgID}`, { orgName: name });
|
||||
navigate(`/org/${orgID}`);
|
||||
},
|
||||
[name, orgID, navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<UpdateOrganizationFormComponent
|
||||
name={name}
|
||||
handleSubmit={handleSubmit}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
25
client/src/controllers/index.ts
Normal file
25
client/src/controllers/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import Navbar from "./Navbar";
|
||||
import MainPage from "./MainPage";
|
||||
import LoginForm from "./LoginForm";
|
||||
import RegisterForm from "./RegisterForm";
|
||||
import EditUserForm from "./EditUserForm";
|
||||
import RecoverForm from "./RecoverForm";
|
||||
import ResetForm from "./ResetForm";
|
||||
import OwnedOrgsDashboard from "./OwnedOrgsDashboard";
|
||||
import Dashboard from "./Dashboard";
|
||||
import UpdateOrganizationForm from "./UpdateOrganizationForm";
|
||||
import MemberOrgsDashboard from "./MemberOrgsDashboard";
|
||||
|
||||
export {
|
||||
Navbar,
|
||||
MainPage,
|
||||
LoginForm,
|
||||
RegisterForm,
|
||||
EditUserForm,
|
||||
RecoverForm,
|
||||
ResetForm,
|
||||
OwnedOrgsDashboard,
|
||||
Dashboard,
|
||||
UpdateOrganizationForm,
|
||||
MemberOrgsDashboard,
|
||||
};
|
||||
8
client/src/environment.d.ts
vendored
Normal file
8
client/src/environment.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
REACT_APP_DOMAIN_ROOT: string;
|
||||
REACT_APP_API_ROOT: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
9
client/src/main.jsx
Normal file
9
client/src/main.jsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
7
client/src/types/ErrorTypes.ts
Normal file
7
client/src/types/ErrorTypes.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export type APIError = {
|
||||
response: {
|
||||
data: {
|
||||
error: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
10
client/src/types/OrganizationTypes.ts
Normal file
10
client/src/types/OrganizationTypes.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { User } from "./UserTypes";
|
||||
|
||||
export type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
owner: User;
|
||||
members: string[];
|
||||
createdAt: string;
|
||||
memberCount: number;
|
||||
};
|
||||
1
client/src/types/ThemeTypes.ts
Normal file
1
client/src/types/ThemeTypes.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type ThemeMode = "light" | "dark";
|
||||
10
client/src/types/UserTypes.ts
Normal file
10
client/src/types/UserTypes.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export type User = {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
pronouns: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
age: number;
|
||||
};
|
||||
4
client/src/types/index.ts
Normal file
4
client/src/types/index.ts
Normal 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";
|
||||
15
client/src/utils/requests.ts
Normal file
15
client/src/utils/requests.ts
Normal 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
25
client/tsconfig.json
Normal 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
11
client/tsconfig.node.json
Normal 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
6
client/vite.config.ts
Normal 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
38
docker-compose.prod.yml
Normal 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: {}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue