Add client
This commit is contained in:
parent
d883bf185b
commit
5c47d7248d
|
|
@ -0,0 +1 @@
|
||||||
|
PORT=21287
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# pull official base image
|
||||||
|
FROM node:18.16.0
|
||||||
|
|
||||||
|
ARG REACT_APP_STAGE
|
||||||
|
|
||||||
|
ENV REACT_APP_STAGE $REACT_APP_STAGE
|
||||||
|
|
||||||
|
# set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copies package.json and package-lock.json to Docker environment
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Installs all node packages
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# Copies everything over to Docker environment
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build for production.
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Install `serve` to run the application.
|
||||||
|
RUN npm install -g serve
|
||||||
|
|
||||||
|
# Uses port which is used by the actual application
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
#CMD [ "npm", "start" ]
|
||||||
|
CMD serve -s build
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# pull official base image
|
||||||
|
FROM node:18.16.0
|
||||||
|
|
||||||
|
# set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copies package.json and package-lock.json to Docker environment
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Add `/app/node_modules/.bin` to $PATH
|
||||||
|
ENV PATH /app/node_modules/.bin:$PATH
|
||||||
|
|
||||||
|
# Installs all node packages
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# Uses port which is used by the actual application
|
||||||
|
EXPOSE 21287
|
||||||
|
|
||||||
|
# Run application
|
||||||
|
CMD [ "npm", "start" ]
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
# Getting Started with Create React App
|
||||||
|
|
||||||
|
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `npm start`
|
||||||
|
|
||||||
|
Runs the app in the development mode.\
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||||
|
|
||||||
|
The page will reload when you make changes.\
|
||||||
|
You may also see any lint errors in the console.
|
||||||
|
|
||||||
|
### `npm test`
|
||||||
|
|
||||||
|
Launches the test runner in the interactive watch mode.\
|
||||||
|
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||||
|
|
||||||
|
### `npm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `build` folder.\
|
||||||
|
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.\
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||||
|
|
||||||
|
### `npm run eject`
|
||||||
|
|
||||||
|
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||||
|
|
||||||
|
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||||
|
|
||||||
|
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||||
|
|
||||||
|
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||||
|
|
||||||
|
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||||
|
|
||||||
|
### Code Splitting
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||||
|
|
||||||
|
### Analyzing the Bundle Size
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||||
|
|
||||||
|
### Making a Progressive Web App
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||||
|
|
||||||
|
### Advanced Configuration
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||||
|
|
||||||
|
### `npm run build` fails to minify
|
||||||
|
|
||||||
|
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
||||||
|
|
||||||
|
|
||||||
|
Preferred Frameworks
|
||||||
|
|
||||||
|
1) Styled components
|
||||||
|
|
||||||
|
2) Recharts
|
||||||
|
|
||||||
|
3) Redux
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,95 @@
|
||||||
|
{
|
||||||
|
"name": "client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "set GENERATE_SOURCEMAP=false && react-scripts build",
|
||||||
|
"build:all": "npm run build:stage && npm run build:prod",
|
||||||
|
"build:prod": "react-scripts build --env=prod && rsync -a --delete build/ user@your-prod-server:/path/to/destination/",
|
||||||
|
"build:stage": "react-scripts build --env=stage && rsync -a --delete build/ user@your-staging-server:/path/to/destination/",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"start": "set GENERATE_SOURCEMAP=false && react-scripts start",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.0",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^5.11.16",
|
||||||
|
"@mui/material": "^5.12.3",
|
||||||
|
"@radix-ui/colors": "^0.1.8",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
|
"@radix-ui/react-form": "^0.0.2",
|
||||||
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
"@radix-ui/react-select": "^1.2.1",
|
||||||
|
"@radix-ui/react-separator": "^1.0.2",
|
||||||
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
|
"@radix-ui/react-toast": "^1.1.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.0.5",
|
||||||
|
"@react-keycloak/web": "^3.4.0",
|
||||||
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^13.4.0",
|
||||||
|
"@types/chart.js": "^2.9.38",
|
||||||
|
"@types/faker": "5.5.9",
|
||||||
|
"@types/file-saver": "^2.0.5",
|
||||||
|
"@types/lodash": "^4.14.195",
|
||||||
|
"@types/node": "^20.1.0",
|
||||||
|
"@types/papaparse": "^5.3.7",
|
||||||
|
"@types/react": "^18.2.6",
|
||||||
|
"@types/react-dom": "^18.2.4",
|
||||||
|
"@types/react-slider": "^1.3.1",
|
||||||
|
"@types/uuid": "^9.0.1",
|
||||||
|
"better-react-mathjax": "^2.0.3",
|
||||||
|
"bootstrap": "^5.2.3",
|
||||||
|
"chart.js": "^4.4.0",
|
||||||
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
|
"faker": "5.5.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
|
"keycloak-js": "^23.0.6",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-jss": "^10.10.0",
|
||||||
|
"react-redux": "^8.1.2",
|
||||||
|
"react-router-dom": "^6.11.1",
|
||||||
|
"react-scripts": "5.0.1",
|
||||||
|
"react-slider": "^2.0.6",
|
||||||
|
"react-spinners": "^0.13.8",
|
||||||
|
"recharts": "^2.10.4",
|
||||||
|
"redux": "^4.2.1",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"reselect": "^5.1.0",
|
||||||
|
"styled-components": "^6.1.7",
|
||||||
|
"typescript": "5.2.2",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"prettier": "^3.1.1"
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "npm run lint && npm run format"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -0,0 +1,39 @@
|
||||||
|
<!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" />
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
<!--
|
||||||
|
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>NCBC Directory</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>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "React App",
|
||||||
|
"name": "Create React App Sample",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
background-color: #282c34;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { ReactKeycloakProvider } from "@react-keycloak/web";
|
||||||
|
import { Chart as RegisterChart, registerables } from "chart.js";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import keycloak from "./keycloak";
|
||||||
|
import ToastDemo from "./shared/basiccomponents/Toast/Toast";
|
||||||
|
import { selectMemberLoading, selectMemberUpdating } from "./store/selectors/member.selectors";
|
||||||
|
import AuthGuard from "./views/AuthGuard/AuthGuard";
|
||||||
|
import Directory from "./views/Directory/Directory";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { memberActions } from "./store/actions/member.actions";
|
||||||
|
import { AppDispatch } from "./store";
|
||||||
|
|
||||||
|
export const DIRECTORY = "/";
|
||||||
|
|
||||||
|
RegisterChart.register(...registerables);
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
const isLoading: boolean = useSelector(selectMemberLoading) === "pending";
|
||||||
|
const isUpdating: boolean = useSelector(selectMemberUpdating) === "pending";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(memberActions.fetchMembers());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReactKeycloakProvider authClient={keycloak}>
|
||||||
|
<ToastDemo isOpen={isLoading} text="Loading members" />
|
||||||
|
<ToastDemo isOpen={isUpdating} text="Saving member" />
|
||||||
|
<AuthGuard>
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path={DIRECTORY} Component={Directory} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</AuthGuard>
|
||||||
|
</ReactKeycloakProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
:root {
|
||||||
|
--test: red;
|
||||||
|
|
||||||
|
--off-white: #f0f2f5;
|
||||||
|
|
||||||
|
--background-grey: #f0f2f5;
|
||||||
|
--background-darker-grey: #e4e4ee;
|
||||||
|
--border-grey: #dadce0;
|
||||||
|
--clickable-grey: #919191;
|
||||||
|
|
||||||
|
--primary-blue: #3774ff;
|
||||||
|
--secondary-blue: #83a9ff;
|
||||||
|
--tertiary-blue: #b9caf0;
|
||||||
|
--sidebar-blue: #2c384a;
|
||||||
|
--sidebar-blue-selected: #b0b7c1;
|
||||||
|
|
||||||
|
--battery-green: #5ec874;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans",
|
||||||
|
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #494F55;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome, Safari, Edge, Opera */
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox */
|
||||||
|
input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
import store, { persistor } from "./store";
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
|
root.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
{/* <PersistGate loading={null} persistor={persistor}> */}
|
||||||
|
<App />
|
||||||
|
{/* </PersistGate> */}
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import Keycloak from "keycloak-js";
|
||||||
|
|
||||||
|
const KeycloakURL = "https://directory.dojo1.e3labs.net/"
|
||||||
|
|
||||||
|
const keycloak = new Keycloak({
|
||||||
|
url: KeycloakURL,
|
||||||
|
realm: "directory",
|
||||||
|
clientId: "React-auth",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default keycloak;
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
const reportWebVitals = (onPerfEntry) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { CRUDService } from "../shared/couchconnector/CouchCrudService";
|
||||||
|
import { Member } from "../types/member";
|
||||||
|
import CouchCrudService from "../shared/couchconnector/CouchCrudService";
|
||||||
|
|
||||||
|
const url = "member_photos";
|
||||||
|
|
||||||
|
const memberCouchDataService = new CouchCrudService<Member>("MemberCouchDataService", url);
|
||||||
|
class MemberCouchService implements CRUDService<Member> {
|
||||||
|
create(item: Member): Promise<Member> {
|
||||||
|
return memberCouchDataService.create(item);
|
||||||
|
}
|
||||||
|
get(): Promise<Member[]> {
|
||||||
|
return memberCouchDataService.get();
|
||||||
|
}
|
||||||
|
getById(id: string): Promise<Member | undefined> {
|
||||||
|
return memberCouchDataService.getById(id);
|
||||||
|
}
|
||||||
|
update(item: Member): Promise<void> {
|
||||||
|
return memberCouchDataService.update(item);
|
||||||
|
}
|
||||||
|
delete(id: string): Promise<void> {
|
||||||
|
return memberCouchDataService.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberCouchService = new MemberCouchService();
|
||||||
|
|
||||||
|
export default memberCouchService;
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Circle } from "@mui/icons-material";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
type CheckBoxProps = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
onSelect: VoidFunction;
|
||||||
|
checked: boolean;
|
||||||
|
};
|
||||||
|
const CheckboxDemo = ({ id, label, checked, onSelect }: CheckBoxProps) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "10px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className="CheckboxRoot"
|
||||||
|
onClick={onSelect}
|
||||||
|
style={{ borderColor: checked ? "#3774ff" : "#5f6368" }}
|
||||||
|
>
|
||||||
|
{checked && <Circle style={{ height: "70%", color: "#3774ff" }} />}
|
||||||
|
</div>
|
||||||
|
<label className="CheckboxLabel" htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default CheckboxDemo;
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CheckboxRoot {
|
||||||
|
background-color: white;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: solid 2px;
|
||||||
|
border-color: #5f6368;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.CheckboxRoot:hover {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CheckboxIndicator {
|
||||||
|
color: var(--violet-11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CheckboxLabel {
|
||||||
|
color: #494F55;
|
||||||
|
/* padding-right: 15px; */
|
||||||
|
/* font-size: 15px; */
|
||||||
|
/* line-height: 1; */
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
type ClickOutsideDetectorProps = {
|
||||||
|
children?: JSX.Element | JSX.Element[];
|
||||||
|
onClickOutside: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ClickOutsideDetector = ({ children, onClickOutside }: ClickOutsideDetectorProps) => {
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOutsideClick = (event: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
onClickOutside();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleOutsideClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleOutsideClick);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div ref={menuRef}>{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClickOutsideDetector;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from "react";
|
||||||
|
import "./styles.css";
|
||||||
|
import { DeleteForever } from "@mui/icons-material";
|
||||||
|
|
||||||
|
type DeleteButtonProps = {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteButton = (props: DeleteButtonProps) => {
|
||||||
|
return (
|
||||||
|
<div className="DeleteButton" style={props.style ? props.style : {}}>
|
||||||
|
<DeleteForever />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteButton;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DeleteButton {
|
||||||
|
background-color: none;
|
||||||
|
color: red;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
width: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DeleteButton:hover {
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { Circle } from "@mui/icons-material";
|
||||||
|
import { DotsVerticalIcon } from "@radix-ui/react-icons";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { BorderedContainer, CenterVertically } from "../../../utils/styles";
|
||||||
|
import HorizontalSeparator from "../HorizontalSeparator/HorizontalSeparator";
|
||||||
|
|
||||||
|
export type action = {
|
||||||
|
icon?: JSX.Element;
|
||||||
|
label?: JSX.Element | string;
|
||||||
|
right?: JSX.Element;
|
||||||
|
action?: VoidFunction;
|
||||||
|
selected?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DropDownIconCSS: React.CSSProperties = {
|
||||||
|
height: "1em",
|
||||||
|
width: "1.5em"
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SelectedCircleProps {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectedCircle = styled(Circle)<SelectedCircleProps>`
|
||||||
|
&& {
|
||||||
|
height: 0.25em;
|
||||||
|
width: 0.4em;
|
||||||
|
color: ${({ selected }) => (selected ? "black" : "white")};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type DropdownProps = {
|
||||||
|
trigger?: JSX.Element;
|
||||||
|
actions: action[];
|
||||||
|
hidden?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
alignLeft?: boolean;
|
||||||
|
selectable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MouseHoverZone = styled.div`
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TriggerContainer = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlankSpacer = styled.div`
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TriggerIcon = styled(DotsVerticalIcon)`
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropdownContainer = styled.div``;
|
||||||
|
|
||||||
|
interface DropdownMenuSpacerProps {
|
||||||
|
alignLeft?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownMenuSpacer = styled.div<DropdownMenuSpacerProps>`
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
${({ alignLeft }) => (alignLeft ? "left: 0%;" : "right: 0%;")}
|
||||||
|
padding: 1em;
|
||||||
|
width: 100%;
|
||||||
|
height: 1em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropdownMenu = styled.div<DropdownMenuSpacerProps>`
|
||||||
|
${BorderedContainer}
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 110%;
|
||||||
|
${({ alignLeft }) => (alignLeft ? "left: 0%;" : "right: 0%;")}
|
||||||
|
padding: 0em 0.5em;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 100%;
|
||||||
|
color: #494f55;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropdownOptionContainer = styled.div``;
|
||||||
|
|
||||||
|
const DropdownOption = styled.div`
|
||||||
|
padding: 0.5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
${CenterVertically}
|
||||||
|
gap: .5em;
|
||||||
|
&:hover {
|
||||||
|
opacity: 45%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropDown = ({ trigger, actions, hidden, isOpen, alignLeft, selectable }: DropdownProps) => {
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MouseHoverZone onMouseLeave={() => setOpen(false)}>
|
||||||
|
<TriggerContainer onClick={() => setOpen(!isOpen)}>
|
||||||
|
{trigger || (hidden ? <BlankSpacer> </BlankSpacer> : <TriggerIcon />)}
|
||||||
|
</TriggerContainer>
|
||||||
|
|
||||||
|
{(open || isOpen) && (
|
||||||
|
<DropdownContainer>
|
||||||
|
<DropdownMenuSpacer onMouseEnter={() => setOpen(true)} alignLeft={alignLeft} />
|
||||||
|
|
||||||
|
<DropdownMenu alignLeft={alignLeft}>
|
||||||
|
{actions.map(({ label, icon, action, selected }, index) => (
|
||||||
|
<DropdownOptionContainer key={index}>
|
||||||
|
<DropdownOption onClick={action}>
|
||||||
|
{selectable && <SelectedCircle selected={selected || false} />}
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</DropdownOption>
|
||||||
|
{index < actions.length - 1 && <HorizontalSeparator margin="0" />}
|
||||||
|
</DropdownOptionContainer>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</DropdownContainer>
|
||||||
|
)}
|
||||||
|
</MouseHoverZone>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropDown;
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuContent,
|
||||||
|
.DropdownMenuSubContent {
|
||||||
|
min-width: 220px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
box-shadow:
|
||||||
|
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||||
|
animation-duration: 400ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuContent[data-side="top"],
|
||||||
|
.DropdownMenuSubContent[data-side="top"] {
|
||||||
|
animation-name: slideDownAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuContent[data-side="right"],
|
||||||
|
.DropdownMenuSubContent[data-side="right"] {
|
||||||
|
animation-name: slideLeftAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuContent[data-side="bottom"],
|
||||||
|
.DropdownMenuSubContent[data-side="bottom"] {
|
||||||
|
animation-name: slideUpAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuContent[data-side="left"],
|
||||||
|
.DropdownMenuSubContent[data-side="left"] {
|
||||||
|
animation-name: slideRightAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuItem,
|
||||||
|
.DropdownMenuCheckboxItem,
|
||||||
|
.DropdownMenuRadioItem,
|
||||||
|
.DropdownMenuSubTrigger {
|
||||||
|
font-size: 1.1em;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--violet11);
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0 5px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
user-select: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuSubTrigger[data-state="open"] {
|
||||||
|
background-color: var(--violet4);
|
||||||
|
color: var(--violet11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuItem[data-disabled],
|
||||||
|
.DropdownMenuCheckboxItem[data-disabled],
|
||||||
|
.DropdownMenuRadioItem[data-disabled],
|
||||||
|
.DropdownMenuSubTrigger[data-disabled] {
|
||||||
|
color: var(--mauve8);
|
||||||
|
/* pointer-events: none; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuItem[data-highlighted],
|
||||||
|
.DropdownMenuCheckboxItem[data-highlighted],
|
||||||
|
.DropdownMenuRadioItem[data-highlighted],
|
||||||
|
.DropdownMenuSubTrigger[data-highlighted] {
|
||||||
|
background-color: var(--violet9);
|
||||||
|
color: var(--violet1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuLabel {
|
||||||
|
padding-left: 25px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 25px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuSeparator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--violet6);
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuItemIndicator {
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
width: 25px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenuArrow {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropDownIconButton {
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 1.5em;
|
||||||
|
width: 1.5em;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--violet11);
|
||||||
|
background: transparent;
|
||||||
|
/* box-shadow: 0 2px 5px var(--blackA9); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropDownIconButton:hover {
|
||||||
|
background-color: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropDownIconButton:focus {
|
||||||
|
/* box-shadow: 0 0 0 2px black; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.RightSlot {
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-highlighted] > .RightSlot {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-disabled] .RightSlot {
|
||||||
|
color: var(--mauve8);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUpAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRightAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDownAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeftAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from "react";
|
||||||
|
import "./styles.css";
|
||||||
|
import { Edit } from "@mui/icons-material";
|
||||||
|
|
||||||
|
type EditButtonProps = {
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditButton = (props: EditButtonProps) => {
|
||||||
|
return <Edit className="EditButton" style={props.style ? props.style : {}} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditButton;
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EditButton {
|
||||||
|
background-color: none;
|
||||||
|
color: rgb(253, 126, 20);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.EditButton:hover {
|
||||||
|
color: #c0c0c0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from "react";
|
||||||
|
import { formInput } from "./Form";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
import { AddCircle } from "@mui/icons-material";
|
||||||
|
|
||||||
|
interface CreateFormProps<Document> {
|
||||||
|
isModalOpen?: boolean;
|
||||||
|
onOpenModal?: VoidFunction;
|
||||||
|
onCloseModal?: VoidFunction;
|
||||||
|
onSubmitForm: (service: Document) => void;
|
||||||
|
document: Document;
|
||||||
|
isDocument: (object: any) => object is Document;
|
||||||
|
formInputs: formInput[];
|
||||||
|
triggerElement: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateFormState = {};
|
||||||
|
|
||||||
|
export default class CreateForm<Document> extends React.Component<CreateFormProps<Document>, CreateFormState> {
|
||||||
|
onFormSubmit = (fields: Partial<Document>) => {
|
||||||
|
const { onSubmitForm: onFormSubmit, document } = this.props;
|
||||||
|
const updatedDocument = { ...document, ...fields };
|
||||||
|
if (this.props.isDocument(updatedDocument)) {
|
||||||
|
onFormSubmit(updatedDocument);
|
||||||
|
} else {
|
||||||
|
throw new Error("updateService did not recieve input of type." + Document);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// console.log("render Secondary Service: " + JSON.stringify(this.state, null, 4))
|
||||||
|
const { formInputs, isModalOpen, onCloseModal, triggerElement } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal<Document>
|
||||||
|
onSubmitForm={this.onFormSubmit}
|
||||||
|
triggerElement={triggerElement}
|
||||||
|
modalTitle={""}
|
||||||
|
modalDescription=""
|
||||||
|
submitFormButtonText="Save"
|
||||||
|
formInputs={formInputs}
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
onCloseModal={onCloseModal}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from "react";
|
||||||
|
import DeleteButton from "../DeleteButton/DeleteButton";
|
||||||
|
import { formInput } from "./Form";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
|
||||||
|
interface DeleteFormProps<Document> {
|
||||||
|
isModalOpen?: boolean;
|
||||||
|
onOpenModal?: VoidFunction;
|
||||||
|
onCloseModal?: VoidFunction;
|
||||||
|
onSubmitForm: VoidFunction;
|
||||||
|
document: Document;
|
||||||
|
documentName?: string;
|
||||||
|
isDocument: (object: any) => object is Document;
|
||||||
|
formInputs: formInput[];
|
||||||
|
triggerText?: string;
|
||||||
|
triggerElement?: JSX.Element;
|
||||||
|
hidden?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteFormState = {};
|
||||||
|
|
||||||
|
export default class DeleteForm<Document> extends React.Component<DeleteFormProps<Document>, DeleteFormState> {
|
||||||
|
render() {
|
||||||
|
// console.log("render Secondary Service: " + JSON.stringify(this.state, null, 4))
|
||||||
|
const {
|
||||||
|
hidden,
|
||||||
|
formInputs,
|
||||||
|
triggerText,
|
||||||
|
style,
|
||||||
|
onSubmitForm,
|
||||||
|
triggerElement,
|
||||||
|
isModalOpen,
|
||||||
|
onCloseModal,
|
||||||
|
documentName
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return hidden ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Modal<Document>
|
||||||
|
onSubmitForm={onSubmitForm}
|
||||||
|
triggerElement={
|
||||||
|
triggerElement ? (
|
||||||
|
triggerElement
|
||||||
|
) : triggerText ? (
|
||||||
|
<div
|
||||||
|
className="Button"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgb(253, 126, 20)",
|
||||||
|
color: "white",
|
||||||
|
...(style || {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{triggerText}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={style}>
|
||||||
|
<DeleteButton />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
modalTitle={`Are you sure you want to delete${documentName ? " " + documentName : ""}`}
|
||||||
|
modalDescription="Deleting is permanent!"
|
||||||
|
submitFormButtonText="Confirm"
|
||||||
|
submitFormButtonColor="red"
|
||||||
|
formInputs={[]}
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
onCloseModal={onCloseModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from "react";
|
||||||
|
import EditButton from "../EditButton/EditButton";
|
||||||
|
import { formInput } from "./Form";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
|
||||||
|
interface EditFormProps<Document> {
|
||||||
|
isModalOpen?: boolean;
|
||||||
|
onOpenModal?: VoidFunction;
|
||||||
|
onCloseModal?: VoidFunction;
|
||||||
|
onSubmitForm: (service: Document) => void;
|
||||||
|
document: Document;
|
||||||
|
isDocument: (object: any) => object is Document;
|
||||||
|
formInputs: formInput[];
|
||||||
|
triggerText?: string;
|
||||||
|
triggerElement?: JSX.Element;
|
||||||
|
hidden?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditFormState = {};
|
||||||
|
|
||||||
|
export default class EditForm<Document> extends React.Component<EditFormProps<Document>, EditFormState> {
|
||||||
|
onFormSubmit = (fields: Partial<Document>) => {
|
||||||
|
const { onSubmitForm: onFormSubmit, document } = this.props;
|
||||||
|
const updatedDocument = { ...document, ...fields };
|
||||||
|
|
||||||
|
if (this.props.isDocument(updatedDocument)) {
|
||||||
|
onFormSubmit(updatedDocument);
|
||||||
|
} else {
|
||||||
|
console.log("updateService from editForm got: " + JSON.stringify(updatedDocument, null, 4));
|
||||||
|
throw new Error("updateService did not recieve input of type.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// console.log("render Secondary Service: " + JSON.stringify(this.state, null, 4))
|
||||||
|
const { hidden, formInputs, triggerText, style, triggerElement, isModalOpen, onCloseModal } = this.props;
|
||||||
|
|
||||||
|
return hidden ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Modal<Document>
|
||||||
|
onSubmitForm={this.onFormSubmit}
|
||||||
|
triggerElement={
|
||||||
|
triggerElement ? (
|
||||||
|
triggerElement
|
||||||
|
) : triggerText ? (
|
||||||
|
<div
|
||||||
|
className="Button"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "rgb(253, 126, 20)",
|
||||||
|
color: "white",
|
||||||
|
...(style || {})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{triggerText}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={style}>
|
||||||
|
<EditButton />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
modalTitle={""}
|
||||||
|
modalDescription=""
|
||||||
|
submitFormButtonText="Save"
|
||||||
|
formInputs={formInputs}
|
||||||
|
isModalOpen={isModalOpen}
|
||||||
|
onCloseModal={onCloseModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
import React from "react";
|
||||||
|
import { SPACE_BETWEEN_HORIZONTALLY } from "../../../utils/styles";
|
||||||
|
import Select from "../Select/Select";
|
||||||
|
|
||||||
|
export type formInput = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
validationMessage: string;
|
||||||
|
inputProps: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||||
|
options?: Dictionary<string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormProps = {
|
||||||
|
onSubmitForm: (fields: Dictionary<any>) => void;
|
||||||
|
submitFormButtonText: string;
|
||||||
|
submitFormButtonColor?: string;
|
||||||
|
formInputs: formInput[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormState = {
|
||||||
|
fields: Dictionary<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generates a Form Message for every invalid matcher. This is used instead of manually creating
|
||||||
|
// a Form Message for every invalid case.
|
||||||
|
// function formMessageMatchAll(message: string) {
|
||||||
|
// const invalidMatchers: ReactForm.ValidityMatcher[] = [
|
||||||
|
// "badInput",
|
||||||
|
// "patternMismatch",
|
||||||
|
// "rangeOverflow",
|
||||||
|
// "rangeUnderflow",
|
||||||
|
// "stepMismatch",
|
||||||
|
// "tooLong",
|
||||||
|
// "tooShort",
|
||||||
|
// "typeMismatch",
|
||||||
|
// "valueMissing"
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// return invalidMatchers.map((matcher) => (
|
||||||
|
// <div key={"match-" + matcher} className="FormMessage">
|
||||||
|
// {message}
|
||||||
|
// </div>
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default class Form extends React.Component<FormProps, FormState> {
|
||||||
|
clickedButtonName = "";
|
||||||
|
|
||||||
|
constructor(props: FormProps | Readonly<FormProps>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
// initialize state from defaultValues provided
|
||||||
|
const fields: { [index: string]: any } = {};
|
||||||
|
|
||||||
|
props.formInputs.forEach((formInput) => {
|
||||||
|
if (formInput.inputProps.defaultValue) fields[formInput.name] = formInput.inputProps.defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = { fields };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFormInput = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
const fields = this.state.fields;
|
||||||
|
this.setState({ fields: { ...fields, ...{ [name]: value } } });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = (event: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.clickedButtonName == this.props.submitFormButtonText) this.props.onSubmitForm(this.state.fields);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = (event: any) => {
|
||||||
|
this.clickedButtonName = event.target.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// console.log("new Form state: " + JSON.stringify(this.state, null, 4))
|
||||||
|
|
||||||
|
const { formInputs, submitFormButtonText, submitFormButtonColor } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="FormRoot" onSubmit={this.onSubmit}>
|
||||||
|
{formInputs.map((formInput) => {
|
||||||
|
return (
|
||||||
|
<div key={formInput.name} className="FormField">
|
||||||
|
{/* Range Input additional label */}
|
||||||
|
{formInput.inputProps.type && formInput.inputProps.type === "range" ? (
|
||||||
|
<label className="FormLabel">
|
||||||
|
{formInput.label} Slider:
|
||||||
|
<em style={{ color: "grey" }}>
|
||||||
|
{this.state.fields[formInput.name] || formInput.inputProps.defaultValue || 1}
|
||||||
|
</em>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: "#494F55" }}>{formInput.label}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Validations */}
|
||||||
|
{/* {formMessageMatchAll(formInput.validationMessage)} */}
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<span>
|
||||||
|
{formInput.options !== undefined ? (
|
||||||
|
<Select
|
||||||
|
values={formInput.options}
|
||||||
|
defaultValue={Object.keys(formInput.options)[0]}
|
||||||
|
onUpdate={(value) => {
|
||||||
|
const fields = this.state.fields;
|
||||||
|
this.setState({ fields: { ...fields, ...{ [formInput.name]: value } } });
|
||||||
|
}}
|
||||||
|
triggerStyle={{ ...SPACE_BETWEEN_HORIZONTALLY, width: "100%", }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className="Input"
|
||||||
|
name={formInput.name}
|
||||||
|
onChange={this.handleFormInput}
|
||||||
|
{...formInput.inputProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div style={{ display: "flex", marginTop: 25 }}>
|
||||||
|
<div style={{ marginRight: 25 }}>
|
||||||
|
<button
|
||||||
|
className={`Button ${submitFormButtonColor || "green"}`}
|
||||||
|
name={submitFormButtonText}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
>
|
||||||
|
{submitFormButtonText}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Dialog from "@radix-ui/react-dialog";
|
||||||
|
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||||
|
import "./styles.css";
|
||||||
|
import Form, { formInput } from "./Form";
|
||||||
|
import { AddCircle } from "@mui/icons-material";
|
||||||
|
|
||||||
|
type FormModalProp<Document> = {
|
||||||
|
isModalOpen?: boolean;
|
||||||
|
onOpenModal?: VoidFunction;
|
||||||
|
onCloseModal?: VoidFunction;
|
||||||
|
onSubmitForm: (fields: Document) => void;
|
||||||
|
triggerElement: JSX.Element;
|
||||||
|
modalTitle: string;
|
||||||
|
modalDescription: string;
|
||||||
|
submitFormButtonText: string;
|
||||||
|
submitFormButtonColor?: string;
|
||||||
|
formInputs: formInput[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function Modal<Document>(props: FormModalProp<Document>) {
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const onOpenChange = (open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
props.onOpenModal && props.onOpenModal();
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
} else {
|
||||||
|
props.onCloseModal && props.onCloseModal();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitForm = (input: any) => {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
props.onSubmitForm(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root
|
||||||
|
open={props.isModalOpen !== undefined ? props.isModalOpen : isDialogOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
>
|
||||||
|
<Dialog.Trigger>{props.triggerElement}</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay className="DialogOverlay" />
|
||||||
|
<Dialog.Content className="DialogContent">
|
||||||
|
<Dialog.Title className="DialogTitle">{props.modalTitle}</Dialog.Title>
|
||||||
|
<Dialog.Description className="DialogDescription">{props.modalDescription}</Dialog.Description>
|
||||||
|
{/* The real content is here. */}
|
||||||
|
<Form
|
||||||
|
onSubmitForm={onSubmitForm}
|
||||||
|
formInputs={props.formInputs}
|
||||||
|
submitFormButtonText={props.submitFormButtonText}
|
||||||
|
submitFormButtonColor={props.submitFormButtonColor}
|
||||||
|
/>
|
||||||
|
<Dialog.Close asChild>
|
||||||
|
<button className="IconButton" aria-label="Close">
|
||||||
|
<Cross2Icon />
|
||||||
|
</button>
|
||||||
|
</Dialog.Close>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/green.css";
|
||||||
|
@import "@radix-ui/colors/red.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button,
|
||||||
|
fieldset,
|
||||||
|
input {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FormRoot {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FormField {
|
||||||
|
display: grid;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FormLabel {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 35px;
|
||||||
|
color: #494F55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FormMessage {
|
||||||
|
font-size: 13px;
|
||||||
|
color: red;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DialogOverlay {
|
||||||
|
background-color: var(--blackA5);
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DialogContent {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
|
||||||
|
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 90vw;
|
||||||
|
max-width: 450px;
|
||||||
|
max-height: 100vh;
|
||||||
|
padding: 25px;
|
||||||
|
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DialogContent:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DialogTitle {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--mauve12);
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DialogDescription {
|
||||||
|
margin: 10px 0 20px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1.5px solid var(--border-grey);
|
||||||
|
background-color: white;
|
||||||
|
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: #2c384a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button:hover {
|
||||||
|
opacity: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.selected {
|
||||||
|
background-color: #b0b7c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.roundedleft {
|
||||||
|
border-radius: 6px 0px 0 6px;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.roundedright {
|
||||||
|
border-radius: 0px 6px 6px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.violet {
|
||||||
|
background-color: white;
|
||||||
|
color: #2c384a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.blue {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.green {
|
||||||
|
background-color: var(--green4);
|
||||||
|
color: var(--green11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.red {
|
||||||
|
background-color: var(--red4);
|
||||||
|
color: var(--red11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.orange {
|
||||||
|
background-color: rgb(253, 126, 20);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconButton {
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--violet11);
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconButton:hover {
|
||||||
|
background-color: var(--violet4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconButton:focus {
|
||||||
|
box-shadow: 0 0 0 2px var(--violet7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Fieldset {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Label {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--violet11);
|
||||||
|
width: 90px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Input {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #494F55;
|
||||||
|
/* box-shadow: 0 0 0 1px var(--violet7); */
|
||||||
|
height: 35px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes overlayShow {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes contentShow {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -48%) scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { UserBubble } from "../../../views/AuthGuard/UserBubble";
|
||||||
|
import { CenterVertically, SpaceBetweenHorizontally } from "../../../utils/styles";
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
title: JSX.Element;
|
||||||
|
actions?: JSX.Element;
|
||||||
|
noActions?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HeaderContainer = styled.div`
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
color: white;
|
||||||
|
text-align: left;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 0.7em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
gap: 1em;
|
||||||
|
`;
|
||||||
|
const Logo = styled.img`
|
||||||
|
max-height: 1.3em;
|
||||||
|
font-size: 2em;
|
||||||
|
`;
|
||||||
|
const Actions = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
gap: 1em;
|
||||||
|
`;
|
||||||
|
const Header = ({ title, actions, noActions }: HeaderProps) => {
|
||||||
|
return (
|
||||||
|
<HeaderContainer>
|
||||||
|
<Title>
|
||||||
|
<Logo src={require("../Icons/e3icon.png")} />
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
<Actions>
|
||||||
|
{actions}
|
||||||
|
{!noActions && <UserBubble />}
|
||||||
|
</Actions>
|
||||||
|
</HeaderContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Separator from "@radix-ui/react-separator";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
type HorizontalSeparatorProps = {
|
||||||
|
margin?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function tableLine(count: number): JSX.Element {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
{Array.from({ length: count }).map((_, index) => (
|
||||||
|
<td key={"tableLine-" + index}>{HorizontalSeparator({})}</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const HorizontalSeparator = ({ margin }: HorizontalSeparatorProps) => (
|
||||||
|
<Separator.Root className="SeparatorRoot" style={{ margin: margin || "15px 0" }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default HorizontalSeparator;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
.SeparatorRoot {
|
||||||
|
background-color: var(--violet6);
|
||||||
|
}
|
||||||
|
.SeparatorRoot[data-orientation="horizontal"] {
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.SeparatorRoot[data-orientation="vertical"] {
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Text {
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||||
|
import "./styles.css";
|
||||||
|
import { Info } from "@mui/icons-material";
|
||||||
|
|
||||||
|
type InfoTooltipProps = {
|
||||||
|
trigger?: JSX.Element;
|
||||||
|
content: string | JSX.Element;
|
||||||
|
hidden?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// const TooltipRoot = Tooltip.Root;
|
||||||
|
// export default TooltipRoot;
|
||||||
|
const InfoTooltip = (props: InfoTooltipProps) => {
|
||||||
|
return props.hidden ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Tooltip.Provider>
|
||||||
|
<Tooltip.Root>
|
||||||
|
<Tooltip.Trigger asChild>
|
||||||
|
<button>{props.trigger || <Info />}</button>
|
||||||
|
</Tooltip.Trigger>
|
||||||
|
<Tooltip.Portal>
|
||||||
|
<Tooltip.Content className="TooltipContent" sideOffset={5}>
|
||||||
|
{props.content}
|
||||||
|
<Tooltip.Arrow className="TooltipArrow" />
|
||||||
|
</Tooltip.Content>
|
||||||
|
</Tooltip.Portal>
|
||||||
|
</Tooltip.Root>
|
||||||
|
</Tooltip.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoTooltip;
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { InfoOutlined } from "@mui/icons-material";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { SpaceBetweenHorizontally } from "../../../utils/styles";
|
||||||
|
|
||||||
|
// Styled components for tooltip
|
||||||
|
const ToggableTooltipContainer = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ToggableTooltipTextProps = {
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToolTipColor = "#ebfaea";
|
||||||
|
|
||||||
|
const ToggableTooltipText = styled.span<ToggableTooltipTextProps>`
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
visibility: ${({ open }) => (open ? "visible" : "hidden")};
|
||||||
|
|
||||||
|
opacity: 1;
|
||||||
|
width: auto;
|
||||||
|
background-color: ${ToolTipColor};
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.9em 1.25em;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 125%;
|
||||||
|
right: 0;
|
||||||
|
margin-left: -60px;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ToggableTooltipArrowProps = {
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToggableTooltipArrow = styled.span<ToggableTooltipArrowProps>`
|
||||||
|
visibility: ${({ open }) => (open ? "visible" : "hidden")};
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: -25%;
|
||||||
|
left: 43%;
|
||||||
|
margin-left: -5px;
|
||||||
|
border-width: 15px 10px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: ${ToolTipColor} transparent transparent transparent;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ToggableTooltipProps = {
|
||||||
|
trigger: JSX.Element;
|
||||||
|
content: string | JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ToggableTooltip component
|
||||||
|
const ToggableTooltip = ({ trigger, content }: ToggableTooltipProps) => {
|
||||||
|
const [open, setOpen] = useState<boolean>(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToggableTooltipContainer onMouseLeave={() => setOpen(false)} onMouseEnter={() => setOpen(true)}>
|
||||||
|
{trigger}
|
||||||
|
<ToggableTooltipText open={open}>
|
||||||
|
<InfoOutlined />
|
||||||
|
{content}
|
||||||
|
</ToggableTooltipText>
|
||||||
|
<ToggableTooltipArrow open={open} />
|
||||||
|
</ToggableTooltipContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToggableTooltip;
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-padding:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
/* or whatever else than inline */
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipContent {
|
||||||
|
border: 1.5px solid var(--border-grey);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--mauve11);
|
||||||
|
background-color: white;
|
||||||
|
box-shadow:
|
||||||
|
hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
|
||||||
|
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||||
|
user-select: none;
|
||||||
|
animation-duration: 5ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipContent[data-state="delayed-open"][data-side="top"] {
|
||||||
|
animation-name: slideDownAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipContent[data-state="delayed-open"][data-side="right"] {
|
||||||
|
animation-name: slideLeftAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipContent[data-state="delayed-open"][data-side="bottom"] {
|
||||||
|
animation-name: slideUpAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipContent[data-state="delayed-open"][data-side="left"] {
|
||||||
|
animation-name: slideRightAndFade;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TooltipArrow {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconButton {
|
||||||
|
font-family: inherit;
|
||||||
|
border-radius: 100%;
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--violet11);
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 0 2px 10px var(--blackA7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconButton:hover {
|
||||||
|
background-color: var(--violet3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.IconButton:focus {
|
||||||
|
box-shadow: 0 0 0 2px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUpAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRightAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDownAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeftAndFade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { MathJax, MathJaxContext } from "better-react-mathjax";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type MathFunctionProps = {
|
||||||
|
func: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MathFunction = ({ func }: MathFunctionProps) => (
|
||||||
|
<MathJaxContext>
|
||||||
|
<MathJax>{func}</MathJax>
|
||||||
|
</MathJaxContext>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MathFunction;
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as RadixSelect from "@radix-ui/react-select";
|
||||||
|
import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons";
|
||||||
|
import "./styles.css";
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
import { GraphOption } from "../../../views/Project/components/CriteriaTab/CriteriaView/styledcomponents";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure the values are rendered the first time! Changing the values leads to weird behaviors.
|
||||||
|
*/
|
||||||
|
export type SelectProps = {
|
||||||
|
label?: string;
|
||||||
|
onUpdate: (value: string) => void;
|
||||||
|
values?: Dictionary<string>;
|
||||||
|
listValues?: string[];
|
||||||
|
defaultValue: string | number;
|
||||||
|
triggerValue?: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
triggerStyle?: React.CSSProperties;
|
||||||
|
prefix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectState = {};
|
||||||
|
|
||||||
|
const SelectItem = (props: RadixSelect.SelectItemProps) => (
|
||||||
|
<RadixSelect.Item className={"SelectItem"} value={props.value}>
|
||||||
|
<RadixSelect.ItemText>{props.children}</RadixSelect.ItemText>
|
||||||
|
<RadixSelect.ItemIndicator className="SelectItemIndicator">
|
||||||
|
<CheckIcon />
|
||||||
|
</RadixSelect.ItemIndicator>
|
||||||
|
</RadixSelect.Item>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default class Select extends React.Component<SelectProps, SelectState> {
|
||||||
|
onValueChange = (value: string) => {
|
||||||
|
this.props.onUpdate(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// console.log("render Select: " + JSON.stringify(this.props, null, 4))
|
||||||
|
|
||||||
|
return this.props.hidden ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<GraphOption.Select>
|
||||||
|
<RadixSelect.Root onValueChange={this.onValueChange} defaultValue={this.props.defaultValue + ""}>
|
||||||
|
<RadixSelect.Trigger className={""} style={this.props.triggerStyle} aria-label="Food">
|
||||||
|
<RadixSelect.Value placeholder={this.props.triggerValue} />
|
||||||
|
<RadixSelect.Icon className="SelectIcon">
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</RadixSelect.Icon>
|
||||||
|
</RadixSelect.Trigger>
|
||||||
|
<RadixSelect.Portal>
|
||||||
|
<RadixSelect.Content className="SelectContent">
|
||||||
|
<RadixSelect.Viewport className="SelectViewport">
|
||||||
|
<RadixSelect.Group>
|
||||||
|
<RadixSelect.Label className="SelectLabel">{this.props.label}</RadixSelect.Label>
|
||||||
|
{this.props.listValues
|
||||||
|
? this.props.listValues.map((value) => (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{(this.props.prefix ?? "") + " " + value}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
: Object.entries(this.props.values!).map(([id, value]) => (
|
||||||
|
<SelectItem key={id} value={id}>
|
||||||
|
{(this.props.prefix ?? "") + " " + value + " "}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</RadixSelect.Group>
|
||||||
|
</RadixSelect.Viewport>
|
||||||
|
</RadixSelect.Content>
|
||||||
|
</RadixSelect.Portal>
|
||||||
|
</RadixSelect.Root>
|
||||||
|
</GraphOption.Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectTrigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 15px;
|
||||||
|
font-size: 1em;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: white;
|
||||||
|
color: var(--violet11);
|
||||||
|
height: 3em;
|
||||||
|
width: 8em;
|
||||||
|
box-shadow: 0 2px 10px var(--blackA7);
|
||||||
|
}
|
||||||
|
.SelectTrigger:hover {
|
||||||
|
background-color: var(--mauve3);
|
||||||
|
}
|
||||||
|
.SelectTrigger:focus {
|
||||||
|
box-shadow: 0 0 0 2px black;
|
||||||
|
}
|
||||||
|
.SelectTrigger[data-placeholder] {
|
||||||
|
color: var(--violet9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectIcon {
|
||||||
|
color: Var(--violet11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectContent {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectViewport {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectItem {
|
||||||
|
line-height: 1;
|
||||||
|
font-size: 1em;
|
||||||
|
color: var(--violet11);
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 25px;
|
||||||
|
padding: 0 35px 0 25px;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.SelectItem[data-disabled] {
|
||||||
|
color: var(--mauve8);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.SelectItem[data-highlighted] {
|
||||||
|
outline: none;
|
||||||
|
background-color: var(--violet9);
|
||||||
|
color: var(--violet1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectLabel {
|
||||||
|
padding: 0 25px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 25px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectSeparator {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--violet6);
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectItemIndicator {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 25px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SelectScrollButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 25px;
|
||||||
|
background-color: white;
|
||||||
|
color: var(--violet11);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Switch from "@radix-ui/react-switch";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
interface SwitchDemoProps {
|
||||||
|
checked: boolean;
|
||||||
|
onClick: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SwitchDemo = ({ checked, onClick }: SwitchDemoProps) => (
|
||||||
|
<Switch.Root className="SwitchRoot" id="airplane-mode" checked={checked} onCheckedChange={() => onClick()}>
|
||||||
|
<Switch.Thumb className="SwitchThumb" />
|
||||||
|
</Switch.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SwitchDemo;
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SwitchRoot {
|
||||||
|
width: 42px;
|
||||||
|
height: 25px;
|
||||||
|
background-color: gray;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1.5px solid var(--border-grey);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SwitchRoot[data-state='checked'] {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SwitchThumb {
|
||||||
|
display: block;
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1.5px solid var(--border-grey);
|
||||||
|
transition: transform 100ms;
|
||||||
|
transform: translateX(2px);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SwitchThumb[data-state='checked'] {
|
||||||
|
transform: translateX(19px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Label {
|
||||||
|
color: #494F55;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
export type Tab = {
|
||||||
|
key: string;
|
||||||
|
trigger: JSX.Element;
|
||||||
|
content: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TabsProps = {
|
||||||
|
content: Tab[];
|
||||||
|
defaultValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TAB_ICON: React.CSSProperties = { height: "50%", width: "50%" };
|
||||||
|
|
||||||
|
const TabsDemo = (props: TabsProps) => (
|
||||||
|
<Tabs.Root className="TabsRoot" defaultValue={props.defaultValue}>
|
||||||
|
<Tabs.List className="TabsList" aria-label="Manage your account">
|
||||||
|
{props.content.map(({ key, trigger }) => (
|
||||||
|
<Tabs.Trigger key={key} className="TabsTrigger" value={key}>
|
||||||
|
{trigger}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
{props.content.map(({ key, content }) => (
|
||||||
|
<Tabs.Content key={key} className="TabsContent" value={key}>
|
||||||
|
{content}
|
||||||
|
</Tabs.Content>
|
||||||
|
))}
|
||||||
|
</Tabs.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TabsDemo;
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/green.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button,
|
||||||
|
fieldset,
|
||||||
|
input {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsRoot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 1px 1px 5px var(--mauve8);
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsList {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 3px solid var(--mauve6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsTrigger {
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 4em;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: start;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--mauve11);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsTrigger:first-child {
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsTrigger:last-child {
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsTrigger:hover {
|
||||||
|
background-color: var(--mauve3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsTrigger[data-state="active"] {
|
||||||
|
color: var(--violet11);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 -1px 0 0 currentColor,
|
||||||
|
0 3px 0 0 currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsTrigger:focus {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabsContent {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 2em;
|
||||||
|
background-color: white;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Text {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--mauve11);
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Fieldset {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Label {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--violet12);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as Toast from "@radix-ui/react-toast";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
type ToastProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RedToastDemo = (props: ToastProps) => {
|
||||||
|
return (
|
||||||
|
<Toast.Provider swipeDirection="right">
|
||||||
|
<Toast.Root
|
||||||
|
className="ToastRoot"
|
||||||
|
open={props.isOpen}
|
||||||
|
style={{
|
||||||
|
border: "#BB3F3F 1px solid",
|
||||||
|
backgroundColor: "hsl(359, 69.5%, 74.3%)",
|
||||||
|
color: "white"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toast.Title className="ToastTitle">
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
props.style || {
|
||||||
|
textAlign: "center",
|
||||||
|
color: "white",
|
||||||
|
fontSize: "1.1em"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.text || "Autosaving..."}
|
||||||
|
</div>
|
||||||
|
</Toast.Title>
|
||||||
|
<Toast.Description asChild>{"Waiting for more changes..."}</Toast.Description>
|
||||||
|
<Toast.Action className="ToastAction" asChild altText="Goto schedule to undo"></Toast.Action>
|
||||||
|
</Toast.Root>
|
||||||
|
<Toast.Viewport className="ToastViewport" />
|
||||||
|
</Toast.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RedToastDemo;
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as Toast from "@radix-ui/react-toast";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
type ToastProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToastDemo = (props: ToastProps) => {
|
||||||
|
return (
|
||||||
|
<Toast.Provider swipeDirection="right">
|
||||||
|
<Toast.Root className="ToastRoot" open={props.isOpen}>
|
||||||
|
<Toast.Title className="ToastTitle green">
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
props.style || {
|
||||||
|
fontSize: "1em",
|
||||||
|
color: "grey",
|
||||||
|
textAlign: "center"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.text || "Autosaving..."}
|
||||||
|
</div>
|
||||||
|
</Toast.Title>
|
||||||
|
<Toast.Description asChild>{"Waiting for more changes..."}</Toast.Description>
|
||||||
|
<Toast.Action className="ToastAction" asChild altText="Goto schedule to undo"></Toast.Action>
|
||||||
|
</Toast.Root>
|
||||||
|
<Toast.Viewport className="ToastViewport" />
|
||||||
|
</Toast.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToastDemo;
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/green.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
@import "@radix-ui/colors/slate.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastViewport {
|
||||||
|
--viewport-padding: 25px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--viewport-padding);
|
||||||
|
gap: 10px;
|
||||||
|
width: auto;
|
||||||
|
max-width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
z-index: 2147483647;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastRoot {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
hsl(206 22% 7% / 35%) 0px 10px 38px -10px,
|
||||||
|
hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||||
|
padding: 15px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "title action" "description action";
|
||||||
|
grid-template-columns: auto max-content;
|
||||||
|
column-gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state="open"] {
|
||||||
|
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state="closed"] {
|
||||||
|
animation: hide 100ms ease-in;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe="move"] {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe="cancel"] {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 200ms ease-out;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe="end"] {
|
||||||
|
animation: swipeOut 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hide {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipeOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastTitle {
|
||||||
|
grid-area: title;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--slate12);
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastDescription {
|
||||||
|
grid-area: description;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--slate11);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastAction {
|
||||||
|
grid-area: action;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Separator from "@radix-ui/react-separator";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
const VerticalSeparator = () => (
|
||||||
|
<Separator.Root className="SeparatorRoot" decorative orientation="vertical" style={{ margin: "0 15px" }} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export default VerticalSeparator;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
.SeparatorRoot {
|
||||||
|
background-color: var(--violet6);
|
||||||
|
}
|
||||||
|
.SeparatorRoot[data-orientation="horizontal"] {
|
||||||
|
height: 1px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.SeparatorRoot[data-orientation="vertical"] {
|
||||||
|
height: 100%;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Text {
|
||||||
|
color: white;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from "react";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
type WidgetProps = {
|
||||||
|
value: any;
|
||||||
|
content?: JSX.Element;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Widget = (props: WidgetProps) => (
|
||||||
|
<div style={props.style || {}}>
|
||||||
|
{props.content}
|
||||||
|
<div
|
||||||
|
className="widget"
|
||||||
|
onDragStart={(e) => {
|
||||||
|
e.dataTransfer.setData("widgetType", JSON.stringify(props.value));
|
||||||
|
}}
|
||||||
|
draggable
|
||||||
|
>
|
||||||
|
{props.value.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Widget;
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Accordion from "@radix-ui/react-accordion";
|
||||||
|
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
export const AccordionTrigger = (props: Accordion.AccordionTriggerProps) => (
|
||||||
|
<Accordion.Header className="AccordionHeader">
|
||||||
|
<Accordion.Trigger className={"AccordionTrigger"} {...props}>
|
||||||
|
{props.children}
|
||||||
|
<ChevronDownIcon className="AccordionChevron" aria-hidden />
|
||||||
|
</Accordion.Trigger>
|
||||||
|
</Accordion.Header>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AccordionContent = (props: Accordion.AccordionContentProps) => (
|
||||||
|
<Accordion.Content className={"AccordionContent"} {...props}>
|
||||||
|
<div className="AccordionContentText">{props.children}</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
);
|
||||||
|
type WidgetBarProps = {
|
||||||
|
children: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WidgetBar = (props: WidgetBarProps) => (
|
||||||
|
<Accordion.Root className="AccordionRoot" type="single" defaultValue="item-0" collapsible>
|
||||||
|
<Accordion.Item className="AccordionItem" value="item-0">
|
||||||
|
<AccordionTrigger>What is this bar?</AccordionTrigger>
|
||||||
|
<AccordionContent>Drag the components from this bar to the table!</AccordionContent>
|
||||||
|
</Accordion.Item>
|
||||||
|
|
||||||
|
{props.children}
|
||||||
|
</Accordion.Root>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default WidgetBar;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from "react";
|
||||||
|
import * as Accordion from "@radix-ui/react-accordion";
|
||||||
|
import "./styles.css";
|
||||||
|
import { AccordionTrigger } from "./WidgetBar";
|
||||||
|
|
||||||
|
type WidgetProps = {
|
||||||
|
triggercontent: JSX.Element;
|
||||||
|
widgets: JSX.Element[];
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WidgetGroup = (props: WidgetProps) => (
|
||||||
|
<Accordion.Item {...props} className="AccordionItem" style={{ position: "relative" }}>
|
||||||
|
<AccordionTrigger> {props.triggercontent} </AccordionTrigger>
|
||||||
|
<Accordion.Content className="AccordionContent">{props.widgets.map((widget) => widget)}</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
);
|
||||||
|
export default WidgetGroup;
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
@import "@radix-ui/colors/blackA.css";
|
||||||
|
@import "@radix-ui/colors/mauve.css";
|
||||||
|
@import "@radix-ui/colors/violet.css";
|
||||||
|
|
||||||
|
/* reset */
|
||||||
|
button,
|
||||||
|
h3 {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionRoot {
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--mauve6);
|
||||||
|
box-shadow: 1px 2px 5px var(--blackA9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionItem {
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 1px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionItem:focus-within {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionHeader {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionTrigger {
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 45px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--violet11);
|
||||||
|
box-shadow: 0 1px 0 var(--mauve6);
|
||||||
|
background-color: white;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionTrigger:hover {
|
||||||
|
background-color: var(--mauve2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionContent {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--mauve11);
|
||||||
|
background-color: transparent;
|
||||||
|
/* background-color: var(--mauve4); */
|
||||||
|
padding-bottom: 1em;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionContent[data-state="open"] {
|
||||||
|
animation: slideDown 300ms ease-out cubic-bezier(0.87, 0, 0.13, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionContent[data-state="closed"] {
|
||||||
|
animation: slideUp 300ms ease-out cubic-bezier(0.87, 0, 0.13, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionContentText {
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionChevron {
|
||||||
|
margin-left: 1em;
|
||||||
|
color: var(--violet10);
|
||||||
|
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.AccordionTrigger[data-state="open"] > .AccordionChevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
height: var(--radix-accordion-content-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--mauve9);
|
||||||
|
color: white;
|
||||||
|
margin: 1em;
|
||||||
|
padding: 0.3em;
|
||||||
|
width: 70%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
|
||||||
|
const envToRemoteMapping: Dictionary<string> = {
|
||||||
|
LOCAL: "",
|
||||||
|
DEV: "",
|
||||||
|
STAGE: "",
|
||||||
|
PROD: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stage = process.env.REACT_APP_STAGE || "LOCAL";
|
||||||
|
const serverUrl = envToRemoteMapping[stage];
|
||||||
|
|
||||||
|
export const useLocal = false && stage == "LOCAL"; //disabled
|
||||||
|
|
||||||
|
export interface CRUDService<T> {
|
||||||
|
create(item: T): Promise<any>;
|
||||||
|
get(): Promise<T[]>;
|
||||||
|
getById(id: string): Promise<T | undefined>;
|
||||||
|
update(item: T): Promise<void>;
|
||||||
|
delete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class CouchCrudService<T extends Dictionary<any>> implements CRUDService<T> {
|
||||||
|
serviceName: string;
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
constructor(serviceName: string, url: string) {
|
||||||
|
this.serviceName = serviceName;
|
||||||
|
this.url = serverUrl + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
create(group: T): Promise<any> {
|
||||||
|
if ("_rev" in group) {
|
||||||
|
console.log(`[${this.serviceName}] CREATE - Removed Rev`);
|
||||||
|
delete group["_rev"];
|
||||||
|
} else {
|
||||||
|
console.log(`[${this.serviceName}] CREATE`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(this.url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(group)
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => data);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): Promise<T[]> {
|
||||||
|
console.log(`[${this.serviceName}] GET`);
|
||||||
|
return fetch(this.url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => data as T[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: string): Promise<T | undefined> {
|
||||||
|
console.log(`[${this.serviceName}] GET BY ID`);
|
||||||
|
|
||||||
|
return fetch(this.url + "/" + id, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => data as T)
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(group: T): Promise<void> {
|
||||||
|
console.log(`[${this.serviceName}] UPDATE`);
|
||||||
|
return fetch(this.url, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(group)
|
||||||
|
})
|
||||||
|
.then((_) => console.log(`[${this.serviceName}] UPDATE SUCCESS`))
|
||||||
|
.catch(() => {
|
||||||
|
throw new Error(`[${this.serviceName}] UPDATE - FAILED`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(id: string): Promise<void> {
|
||||||
|
console.log(`[${this.serviceName}] DELETE: ${id}`);
|
||||||
|
return fetch(this.url + "/" + id, {
|
||||||
|
method: "DELETE"
|
||||||
|
})
|
||||||
|
.then((_) => console.log(`[${this.serviceName}] DELETE SUCCESS`))
|
||||||
|
.catch(() => {
|
||||||
|
throw new Error(`[${this.serviceName}] DELETE: ${id} - FAILED`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// const monthNames = ["January", "February", "March", "April", "May", "June",
|
||||||
|
// "July", "August", "September", "October", "November", "December"
|
||||||
|
// ];
|
||||||
|
|
||||||
|
function convertMilitaryToRegularTime(militaryTime: string): string {
|
||||||
|
const [hours, minutes] = militaryTime.split(":");
|
||||||
|
let regularHours = parseInt(hours, 10);
|
||||||
|
let suffix = "AM";
|
||||||
|
|
||||||
|
if (regularHours >= 12) {
|
||||||
|
suffix = "PM";
|
||||||
|
if (regularHours > 12) {
|
||||||
|
regularHours -= 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedMinutes = minutes.padStart(2, "0");
|
||||||
|
|
||||||
|
return `${regularHours}:${formattedMinutes} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentTime() {
|
||||||
|
const currentDate: Date = new Date();
|
||||||
|
|
||||||
|
const hours: number = currentDate.getHours();
|
||||||
|
const minutes: number = currentDate.getMinutes();
|
||||||
|
// const seconds: number = currentDate.getSeconds();
|
||||||
|
const day: number = currentDate.getDate();
|
||||||
|
const month: string = currentDate.getMonth() + 1 + "";
|
||||||
|
const year: number = currentDate.getFullYear();
|
||||||
|
const regularTime: string = convertMilitaryToRegularTime(`${hours}:${minutes}`);
|
||||||
|
|
||||||
|
const formattedDate: string = `${month.padStart(2, "0")}/${day}/${year} - ${regularTime}`;
|
||||||
|
// console.log(formattedDate);
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getCurrentTime;
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { styled, css } from "styled-components";
|
||||||
|
import { COLORS, CenterVertically } from "../../utils/styles";
|
||||||
|
|
||||||
|
export const ButtonCSS = css`
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1.5px solid var(--border-grey);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #2c384a;
|
||||||
|
display: flex;
|
||||||
|
font-size: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
white-space: nowrap;
|
||||||
|
gap: .5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
selected {
|
||||||
|
background-color: #b0b7c1;
|
||||||
|
}
|
||||||
|
roundedleft {
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
roundedright {
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Button = styled.button`
|
||||||
|
${ButtonCSS}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BlueButton = styled(Button)`
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #007bff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GreenButton = styled(Button)`
|
||||||
|
background-color: var(--green4);
|
||||||
|
color: var(--green11);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RedButton = styled(Button)`
|
||||||
|
background-color: var(--red4);
|
||||||
|
color: var(--red11);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OrangeButton = styled(Button)`
|
||||||
|
background-color: #fd7e14;
|
||||||
|
color: #fff;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VioletButtonCSS = css`
|
||||||
|
${ButtonCSS}
|
||||||
|
background-color: #fff;
|
||||||
|
color: #2c384a;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VioletButton = styled(Button)`
|
||||||
|
background-color: #fff;
|
||||||
|
color: #2c384a;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ToggableButtonProps {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggableButton = styled(Button)<ToggableButtonProps>`
|
||||||
|
background-color: ${({ selected }) => (selected ? COLORS.SidebarBlueSelected : "white")};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ToggableIconButton = styled(ToggableButton)<ToggableButtonProps>`
|
||||||
|
padding: 0.4em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ViewToggleContainer = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: right;
|
||||||
|
justify-content: flex-end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LeftViewToggleButton = styled(ToggableButton)`
|
||||||
|
border-radius: 6px 0px 0 6px;
|
||||||
|
border-right: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RightViewToggleButton = styled(ToggableButton)`
|
||||||
|
border-radius: 0px 6px 6px 0px;
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { CenterVertically, SpaceBetweenHorizontally } from "../../utils/styles";
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { CenterVertically, SpaceBetweenHorizontally } from "../../utils/styles";
|
||||||
|
import { VioletButtonCSS } from "./Button";
|
||||||
|
|
||||||
|
export const PageHeader = {
|
||||||
|
Container: styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
width: 30%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: #e8e8e8;
|
||||||
|
font-weight: 400;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
|
||||||
|
Link: styled(Link)`
|
||||||
|
/* ${VioletButtonCSS} */
|
||||||
|
text-decoration: none;
|
||||||
|
color: #e8e8e8;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { BorderedContainer, CenterVertically, SpaceBetweenHorizontally } from "../../utils/styles";
|
||||||
|
|
||||||
|
const SearchBarContainer = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
${BorderedContainer}
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
color: #494F55;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
padding: 5px;
|
||||||
|
width: 30em; /* Adjust width as needed */
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SearchInput = styled.input`
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const iconSize = "1.25";
|
||||||
|
const SearchIcon = styled(MagnifyingGlassIcon)`
|
||||||
|
height: ${iconSize}em;
|
||||||
|
width: ${iconSize}em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
placeholder?: string;
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchBar: React.FC<SearchBarProps> = ({ placeholder, onChange }) => {
|
||||||
|
return (
|
||||||
|
<SearchBarContainer>
|
||||||
|
<SearchIcon />
|
||||||
|
<SearchInput type="text" placeholder={placeholder} onChange={onChange} />
|
||||||
|
</SearchBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import styled, { css } from "styled-components";
|
||||||
|
import {
|
||||||
|
BorderedContainer,
|
||||||
|
COLORS,
|
||||||
|
CenterHorizontally,
|
||||||
|
CenterVertically,
|
||||||
|
FlexBoxes,
|
||||||
|
Hoverable,
|
||||||
|
ProjectMainChild,
|
||||||
|
SpaceBetweenHorizontally
|
||||||
|
} from "../../utils/styles";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
|
export const TabTriggerGroup = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
gap: 2em;
|
||||||
|
/* width: 88%;
|
||||||
|
margin: auto; */
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabContentContainer = styled.section`
|
||||||
|
margin: 1em auto;
|
||||||
|
width: 88%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabTriggerWrapper = styled.div<TabTriggerWrapperProps>`
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
color: ${({ selected }) => (selected ? COLORS.PrimaryBlue : COLORS.ClickableGrey)};
|
||||||
|
|
||||||
|
${({ selected }) => selected && `animation: borderAnimation 0.3s linear;`}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 75%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabTriggerIcon = css`
|
||||||
|
&& {
|
||||||
|
width: 20%;
|
||||||
|
height: 50%;
|
||||||
|
margin: 0 5% 0 0;
|
||||||
|
${CenterVertically}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface TabTriggerWrapperProps {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
export const TabTitle = styled.h4<TabTriggerWrapperProps>`
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
border-bottom: 3px solid ${({ selected }) => (selected ? COLORS.PrimaryBlue : "none")};
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EmptyTab = styled.div`
|
||||||
|
${ProjectMainChild}
|
||||||
|
padding: 2em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabContent = {
|
||||||
|
Container: styled.div`
|
||||||
|
${ProjectMainChild}
|
||||||
|
${BorderedContainer}
|
||||||
|
`,
|
||||||
|
Header: styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
height: 3em;
|
||||||
|
padding: 2em 1.7em;
|
||||||
|
background: ${COLORS.BackgroundGrey};
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
color: ${COLORS.SideBarBlue};
|
||||||
|
gap: 10px;
|
||||||
|
border-bottom: 1.5px solid ${COLORS.BorderGrey};
|
||||||
|
`,
|
||||||
|
ActionHeader: styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
${BorderedContainer}
|
||||||
|
height: 3em;
|
||||||
|
padding: 2em 1.7em;
|
||||||
|
background: ${COLORS.BackgroundGrey};
|
||||||
|
color: ${COLORS.SideBarBlue};
|
||||||
|
gap: 10px;
|
||||||
|
`,
|
||||||
|
HeaderTitle: styled.strong`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
HeaderTitleNavOption: styled(Hoverable)`
|
||||||
|
font-weight: lighter;
|
||||||
|
|
||||||
|
&& {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
HeaderTitleTag: styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${CenterHorizontally}
|
||||||
|
background: ${COLORS.SidebarBlueSelected};
|
||||||
|
margin-top: 0.3em;
|
||||||
|
padding: 0.05em 1em 0.2em;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
`,
|
||||||
|
HeaderActions: {
|
||||||
|
Wrapper: styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
|
||||||
|
max-width: 88%;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-align: right;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1em;
|
||||||
|
`,
|
||||||
|
DropDownActionTrigger: styled(Button)`
|
||||||
|
padding-right: 0.4em;
|
||||||
|
`,
|
||||||
|
DropDownActionOption: styled.button`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 45%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
Body: styled.div`
|
||||||
|
${FlexBoxes}
|
||||||
|
padding: 1em 1em 1.5em;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { createAsyncThunk } from "@reduxjs/toolkit";
|
||||||
|
import memberCouchService from "../../services/memberCouchService";
|
||||||
|
import { Member } from "../../types/member";
|
||||||
|
import { requestDelay } from "../../utils/RequestDelay";
|
||||||
|
import getFormattedNow from "../../utils/TimeFormater";
|
||||||
|
|
||||||
|
const createMember = createAsyncThunk("members/createMembers", async (member: Member) => {
|
||||||
|
await requestDelay();
|
||||||
|
await memberCouchService.create(member);
|
||||||
|
return { member };
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchMembers = createAsyncThunk("members/fetchMembers", async () => {
|
||||||
|
await requestDelay();
|
||||||
|
const response = await memberCouchService.get();
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMember = createAsyncThunk("members/updateMember", async (member: Member) => {
|
||||||
|
await requestDelay();
|
||||||
|
|
||||||
|
console.log("updateMember - " + member.name);
|
||||||
|
|
||||||
|
member.lastUpdated = getFormattedNow();
|
||||||
|
await memberCouchService.update(member);
|
||||||
|
return member;
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMember = createAsyncThunk("members/deleteMemberWithoutChildren", async (id: string) => {
|
||||||
|
const response = await memberCouchService.delete(id);
|
||||||
|
return id;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const memberActions = {
|
||||||
|
createMember,
|
||||||
|
fetchMembers,
|
||||||
|
updateMember,
|
||||||
|
deleteMember
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
|
||||||
|
import rootReducer from "./reducers";
|
||||||
|
import { persistStore } from "redux-persist";
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
|
reducer: rootReducer
|
||||||
|
});
|
||||||
|
|
||||||
|
// export const persistor = persistStore(store);
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
export type AppThunk<ReturnType = void> = ThunkAction<ReturnType, RootState, unknown, Action<string>>;
|
||||||
|
|
||||||
|
export default store;
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { combineReducers } from "@reduxjs/toolkit";
|
||||||
|
import { persistReducer } from "redux-persist";
|
||||||
|
import storage from "redux-persist/es/storage";
|
||||||
|
import membersReducer from "./member.reducer";
|
||||||
|
import usersReducer from "./user.reducer";
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
usersState: usersReducer,
|
||||||
|
membersState: membersReducer
|
||||||
|
});
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: "root",
|
||||||
|
storage
|
||||||
|
};
|
||||||
|
|
||||||
|
const persistedRootReducer = persistReducer(persistConfig, rootReducer);
|
||||||
|
|
||||||
|
export default persistedRootReducer;
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
import { couchDocumentListToDictionary } from "../../types/CouchDocument";
|
||||||
|
import { Member } from "../../types/member";
|
||||||
|
import { memberActions } from "../actions/member.actions";
|
||||||
|
|
||||||
|
export interface MembersReducer {
|
||||||
|
membersState: MembersState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MembersState {
|
||||||
|
members: Dictionary<Member>;
|
||||||
|
loading: "idle" | "pending" | "succeeded" | "failed";
|
||||||
|
updating: "idle" | "pending" | "succeeded" | "failed";
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
members: {},
|
||||||
|
loading: "idle",
|
||||||
|
updating: "idle"
|
||||||
|
} as MembersState;
|
||||||
|
|
||||||
|
const membersSlice = createSlice({
|
||||||
|
name: "members",
|
||||||
|
initialState,
|
||||||
|
reducers: {},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(memberActions.createMember.fulfilled, (state, action) => {
|
||||||
|
const { member } = action.payload;
|
||||||
|
const members = {
|
||||||
|
...state.members,
|
||||||
|
[member._id]: member
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
members
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addCase(memberActions.fetchMembers.pending, (state, action) => {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: "pending"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addCase(memberActions.fetchMembers.fulfilled, (state, action) => {
|
||||||
|
console.log("fetchMembers action recieved: " + JSON.stringify(action.payload, null, 4));
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
loading: "succeeded",
|
||||||
|
members: couchDocumentListToDictionary(action.payload)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addCase(memberActions.updateMember.pending, (state, action) => {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
updating: "pending"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addCase(memberActions.updateMember.fulfilled, (state, action) => {
|
||||||
|
const updatedMember: Member = action.payload;
|
||||||
|
const members = {
|
||||||
|
...state.members,
|
||||||
|
[updatedMember._id]: updatedMember
|
||||||
|
};
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
updating: "succeeded",
|
||||||
|
members
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addCase(memberActions.deleteMember.fulfilled, (state, action) => {
|
||||||
|
const toDeleteId: string = action.payload;
|
||||||
|
const members = { ...state.members };
|
||||||
|
delete members[toDeleteId];
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
members
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const membersReducer = membersSlice.reducer;
|
||||||
|
|
||||||
|
export default membersReducer;
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export interface UsersReducer {
|
||||||
|
usersState: UsersState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersState {
|
||||||
|
username: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
username: "",
|
||||||
|
firstname: "",
|
||||||
|
lastname: ""
|
||||||
|
} as UsersState;
|
||||||
|
|
||||||
|
const usersSlice = createSlice({
|
||||||
|
name: "user",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
updateUsername(state, action: PayloadAction<string>) {
|
||||||
|
state.username = action.payload;
|
||||||
|
},
|
||||||
|
updateFirstname(state, action: PayloadAction<string>) {
|
||||||
|
state.firstname = action.payload;
|
||||||
|
},
|
||||||
|
updateLastname(state, action: PayloadAction<string>) {
|
||||||
|
state.lastname = action.payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { updateUsername, updateFirstname, updateLastname } = usersSlice.actions;
|
||||||
|
|
||||||
|
const usersReducer = usersSlice.reducer;
|
||||||
|
|
||||||
|
export default usersReducer;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { MembersReducer } from "../reducers/member.reducer";
|
||||||
|
|
||||||
|
export const selectMembers = (state: MembersReducer) => state.membersState.members;
|
||||||
|
export const selectMember = (id: string) => (state: MembersReducer) => state.membersState.members[id];
|
||||||
|
export const selectMemberLoading = (state: MembersReducer) => state.membersState.loading;
|
||||||
|
export const selectMemberUpdating = (state: MembersReducer) => state.membersState.updating;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { UsersReducer } from "../reducers/user.reducer";
|
||||||
|
|
||||||
|
export const selectUsername = (state: UsersReducer) => state.usersState.username;
|
||||||
|
|
||||||
|
export const selectFirstname = (state: UsersReducer) => state.usersState.firstname;
|
||||||
|
|
||||||
|
export const selectLastname = (state: UsersReducer) => state.usersState.lastname;
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export type couchDocument = Dictionary<any> & {
|
||||||
|
_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createUUID = () => {
|
||||||
|
return uuidv4();
|
||||||
|
};
|
||||||
|
|
||||||
|
export function couchDocumentListToDictionary<T extends couchDocument>(objects: T[]): Dictionary<T> {
|
||||||
|
return objects.reduce((sum: Dictionary<T>, object: T) => {
|
||||||
|
if (object) sum[object._id] = object;
|
||||||
|
return sum;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import uuidGenerator from "../utils/uuidGenerator";
|
||||||
|
import { couchDocument } from "./CouchDocument";
|
||||||
|
|
||||||
|
export type Member = couchDocument & {
|
||||||
|
apt: string;
|
||||||
|
birthday: string;
|
||||||
|
city: string;
|
||||||
|
communityGroup: string;
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
houseNumber: string;
|
||||||
|
lastName: string;
|
||||||
|
marriedTo: string;
|
||||||
|
spouseIsMember: string;
|
||||||
|
memberSince: string;
|
||||||
|
memberStatus: string;
|
||||||
|
phone: string;
|
||||||
|
pictureFilename: string;
|
||||||
|
roles: string;
|
||||||
|
state: string;
|
||||||
|
streetName: string;
|
||||||
|
streetType: string;
|
||||||
|
userLogin: string;
|
||||||
|
zipCode: string;
|
||||||
|
// image: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MemberUtils = {
|
||||||
|
getDefault: (): Member => ({
|
||||||
|
_id: uuidGenerator(),
|
||||||
|
apt: "",
|
||||||
|
birthday: "",
|
||||||
|
city: "",
|
||||||
|
communityGroup: "",
|
||||||
|
email: "",
|
||||||
|
firstName: "",
|
||||||
|
houseNumber: "",
|
||||||
|
lastName: "",
|
||||||
|
marriedTo: "",
|
||||||
|
spouseIsMember: "",
|
||||||
|
memberSince: "",
|
||||||
|
memberStatus: "",
|
||||||
|
phone: "",
|
||||||
|
pictureFilename: "",
|
||||||
|
roles: "",
|
||||||
|
state: "",
|
||||||
|
streetName: "",
|
||||||
|
streetType: "",
|
||||||
|
userLogin: "",
|
||||||
|
zipCode: "",
|
||||||
|
// image: ""
|
||||||
|
}),
|
||||||
|
|
||||||
|
validate: (object: any): object is Member => "_id" in object,
|
||||||
|
|
||||||
|
inputs: (Member?: Member) => ({
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
label: "Member Name",
|
||||||
|
validationMessage: "Please enter the name of the Member",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
compareByName: (member: Member, other: Member) => {
|
||||||
|
return member.name.localeCompare(other.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Create our number formatter.
|
||||||
|
const priceFormatter = new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD"
|
||||||
|
|
||||||
|
// These options are needed to round to whole numbers if that's what you want.
|
||||||
|
//minimumFractionDigits: 0, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
|
||||||
|
//maximumFractionDigits: 0, // (causes 2500.99 to be printed as $2,501)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default priceFormatter;
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
export default function remove<T>(list: T[], toRemove: T) {
|
||||||
|
return list.filter(item => item !== toRemove)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const requestDelay = () => new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as Papa from "papaparse";
|
||||||
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
|
export const downloadTableAsCsv = (data: string[][], fileName: string) => {
|
||||||
|
const csv = Papa.unparse(data);
|
||||||
|
const csvData = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||||
|
saveAs(csvData, fileName);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
// const monthNames = ["January", "February", "March", "April", "May", "June",
|
||||||
|
// "July", "August", "September", "October", "November", "December"
|
||||||
|
// ];
|
||||||
|
|
||||||
|
function convertMilitaryToRegularTime(militaryTime: string): string {
|
||||||
|
const [hours, minutes] = militaryTime.split(":");
|
||||||
|
let regularHours = parseInt(hours, 10);
|
||||||
|
let suffix = "AM";
|
||||||
|
|
||||||
|
if (regularHours >= 12) {
|
||||||
|
suffix = "PM";
|
||||||
|
if (regularHours > 12) {
|
||||||
|
regularHours -= 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedMinutes = minutes.padStart(2, "0");
|
||||||
|
|
||||||
|
return `${regularHours}:${formattedMinutes} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFormattedNow() {
|
||||||
|
const currentDate: Date = new Date();
|
||||||
|
|
||||||
|
const hours: number = currentDate.getHours();
|
||||||
|
const minutes: number = currentDate.getMinutes();
|
||||||
|
// const seconds: number = currentDate.getSeconds();
|
||||||
|
const day: number = currentDate.getDate();
|
||||||
|
const month: string = currentDate.getMonth() + 1 + "";
|
||||||
|
const year: number = currentDate.getFullYear();
|
||||||
|
const regularTime: string = convertMilitaryToRegularTime(`${hours}:${minutes}`);
|
||||||
|
|
||||||
|
const formattedDate: string = `${month.padStart(2, "0")}/${day}/${year} - ${regularTime}`;
|
||||||
|
// console.log(formattedDate);
|
||||||
|
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFormattedNowFromToday(formattedNow: string): boolean {
|
||||||
|
return getDateFromFormattedNow(getFormattedNow()) === getDateFromFormattedNow(formattedNow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDateFromFormattedNow(formattedNow: string): string {
|
||||||
|
return formattedNow.slice(0, formattedNow.indexOf(" - "));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimeFromFormattedNow(formattedNow: string): string {
|
||||||
|
return formattedNow.slice(formattedNow.indexOf(" - ") + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getFormattedNow;
|
||||||
|
|
||||||
|
export const dateFromString = (dateString: string) => {
|
||||||
|
const regex = /^(\d{1,2})\/(\d{1,2})\/(\d{4}) - (\d{1,2}):(\d{1,2}) (AM|PM)$/i;
|
||||||
|
const match = dateString.match(regex);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error("Invalid date format");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, month, day, year, hours, minutes, amPm] = match;
|
||||||
|
const hour = amPm.toUpperCase() === "PM" && hours !== "12" ? parseInt(hours) + 12 : parseInt(hours);
|
||||||
|
const date = new Date(+year, +month - 1, +day, hour, parseInt(minutes));
|
||||||
|
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDisplayDateFromString(lastUpdated: string) {
|
||||||
|
return isFormattedNowFromToday(lastUpdated)
|
||||||
|
? getTimeFromFormattedNow(lastUpdated)
|
||||||
|
: getDateFromFormattedNow(lastUpdated);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function countDigitsToRightOfDecimal(number: number): number {
|
||||||
|
const numberString = number.toString();
|
||||||
|
if (numberString.includes(".")) {
|
||||||
|
const decimalPart = numberString.split(".")[1];
|
||||||
|
return decimalPart.length;
|
||||||
|
}
|
||||||
|
return 0; // If there is no decimal point, return 0.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
export const LIGHT_FONT: React.CSSProperties = {
|
||||||
|
color: "darkgrey"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const THIN_FONT: React.CSSProperties = {
|
||||||
|
fontWeight: "300"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BOLD_FONT: React.CSSProperties = {
|
||||||
|
fontWeight: "700"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MEDIUM_FONT: React.CSSProperties = {
|
||||||
|
fontSize: "1em"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LARGE_FONT: React.CSSProperties = {
|
||||||
|
fontSize: "2em"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EXTRA_LARGE_FONT: React.CSSProperties = {
|
||||||
|
fontSize: "3em"
|
||||||
|
};
|
||||||
|
export const CENTER_VERTICALLY: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CENTER_HORIZONTALLY: React.CSSProperties = {
|
||||||
|
justifyContent: "center",
|
||||||
|
display: "flex"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STACK_FROM_BOTTOM: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
flexDirection: "column"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SPACE_EVENLY_HORIZONTALLY: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-evenly"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SPACE_BETWEEN_HORIZONTALLY: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ALIGN_LEFT: React.CSSProperties = {
|
||||||
|
textAlign: "left"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ALIGN_RIGHT: React.CSSProperties = {
|
||||||
|
textAlign: "right",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
display: "flex"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ALIGN_CENTER: React.CSSProperties = {
|
||||||
|
textAlign: "center"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MARGIN_TOP: React.CSSProperties = {
|
||||||
|
marginTop: "2em"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MARGIN_BOTTOM: React.CSSProperties = {
|
||||||
|
marginBottom: "2em"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MARGIN_RIGHT: React.CSSProperties = {
|
||||||
|
marginRight: "2em"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HALF_WIDTH: React.CSSProperties = {
|
||||||
|
width: "50%"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FLEX_BOXES: React.CSSProperties = {
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BORDER: React.CSSProperties = {
|
||||||
|
border: "solid red 1px"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BORDER_COLOR = (color: string): React.CSSProperties => ({
|
||||||
|
border: `solid ${color} .1em`
|
||||||
|
});
|
||||||
|
|
||||||
|
export enum COLORS {
|
||||||
|
OffWhite = "#F0F2F5",
|
||||||
|
OffWhiteSelected = "#505e72",
|
||||||
|
|
||||||
|
BackgroundGrey = "#F0F2F5",
|
||||||
|
DarkerBackgroundGrey = "#d3d4d5a0",
|
||||||
|
BorderGrey = "#dadce0",
|
||||||
|
ClickableGrey = "#919191",
|
||||||
|
|
||||||
|
Orange = "rgb(253, 126, 20)",
|
||||||
|
|
||||||
|
Blue = "#007bff",
|
||||||
|
PrimaryBlue = "#3774ff",
|
||||||
|
SecondaryBlue = "#83a9ff",
|
||||||
|
SideBarBlue = "#2C384A",
|
||||||
|
SidebarBlueSelected = "#B0B7C1",
|
||||||
|
SideBarOffBlue = "#52688A"
|
||||||
|
}
|
||||||
|
|
||||||
|
import { styled, css } from "styled-components";
|
||||||
|
|
||||||
|
// export const LIGHT_FONT = css`;
|
||||||
|
// color: "darkgrey";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const THIN_FONT = css`
|
||||||
|
// font-weight: 300;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const BOLD_FONT = css`
|
||||||
|
// font-weight: 700;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const MEDIUM_FONT = css`
|
||||||
|
// font-size: 1em;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const LARGE_FONT = css`
|
||||||
|
// font-size: 2em;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const EXTRA_LARGE_FONT = css`
|
||||||
|
// font-size: 3em;
|
||||||
|
// `;
|
||||||
|
|
||||||
|
export const CenterVertically = css`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CenterHorizontally = css`
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StackFromBottom = css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SpaceEvenlyHorizontally = css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SpaceBetweenHorizontally = css`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// export const ALIGN_LEFT = css`
|
||||||
|
// text-align: "left";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const ALIGN_RIGHT = css`
|
||||||
|
// text-align: "right";
|
||||||
|
// justify-content: "flex-end";
|
||||||
|
// display: "flex";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
export const AlignCenter = css`
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// export const MARGIN_TOP = css`
|
||||||
|
// margin-top: "2em";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const MARGIN_BOTTOM = css`
|
||||||
|
// margin-bottom: "2em";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const MARGIN_RIGHT = css`
|
||||||
|
// margin-right: "2em";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
export const FlexBoxes = css`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// export const BORDER = css`
|
||||||
|
// border: "solid red 1px";
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// export const BORDER_COLOR = (color: string): React.CSSProperties => ({
|
||||||
|
// border: `solid ${color} .1em`
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export enum COLORS {
|
||||||
|
// OffWhite = "#F0F2F5",
|
||||||
|
// OffWhiteSelected = "#505e72",
|
||||||
|
|
||||||
|
// BackgroundGrey = "#F0F2F5",
|
||||||
|
// DarkerBackgroundGrey = "#d3d4d5a0",
|
||||||
|
// BorderGrey = "#dadce0",
|
||||||
|
// ClickableGrey = "#494F55",
|
||||||
|
|
||||||
|
// Orange = "rgb(253, 126, 20)",
|
||||||
|
|
||||||
|
// Blue = "#007bff",
|
||||||
|
// PrimaryBlue = "#3774ff",
|
||||||
|
// SecondaryBlue = "#83a9ff",
|
||||||
|
// SideBarBlue = "#2C384A",
|
||||||
|
// SidebarBlueSelected = "#B0B7C1",
|
||||||
|
// SideBarOffBlue = "#52688A"
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const Input = css`
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #494f55;
|
||||||
|
/* box-shadow: 0 0 0 1px var(--violet7); */
|
||||||
|
height: 35px;
|
||||||
|
background-color: white;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ProjectMainChild = css`
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
background-color: white;
|
||||||
|
min-height: 0em;
|
||||||
|
/* padding-bottom: 3em; */
|
||||||
|
`;
|
||||||
|
export const BorderedContainer = css`
|
||||||
|
border: 1.5px solid var(--border-grey);
|
||||||
|
border-radius: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BorderRow = css`
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const HoverableRow = css`
|
||||||
|
background-color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Hoverable = styled.div`
|
||||||
|
opacity: 100%;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 45%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const HoverableRounded = styled.div`
|
||||||
|
${CenterHorizontally}
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
padding: 0.2em;
|
||||||
|
margin-right: auto 0.5em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--mauve6);
|
||||||
|
background-color: var(--mauve6);
|
||||||
|
box-shadow: 1px 1px 5px var(--mauve6);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const HorizontalLine = styled.div`
|
||||||
|
border-bottom: 1.5px solid #ccc;
|
||||||
|
/* Adjust the color and thickness as needed */
|
||||||
|
margin: 10px 0;
|
||||||
|
/* Adjust the margin as needed */
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StrongHorizontalLine = styled.div`
|
||||||
|
border-bottom: 1.3px solid black;
|
||||||
|
/* Adjust the color and thickness as needed */
|
||||||
|
margin: 10px 0;
|
||||||
|
/* Adjust the margin as needed */
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const uuidGenerator = () => {
|
||||||
|
return uuidv4();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default uuidGenerator;
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import { useKeycloak } from "@react-keycloak/web";
|
||||||
|
import { FC, ReactNode, useEffect, useState } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Header from "../../shared/basiccomponents/Header/Header";
|
||||||
|
import { stage } from "../../shared/couchconnector/CouchCrudService";
|
||||||
|
import { Button } from "../../shared/styledcomponents/Button";
|
||||||
|
import { PageHeader } from "../../shared/styledcomponents/PageHeader";
|
||||||
|
import { updateFirstname, updateLastname, updateUsername } from "../../store/reducers/user.reducer";
|
||||||
|
import { CenterHorizontally, CenterVertically } from "../../utils/styles";
|
||||||
|
import { RedirectPage } from "./Redirect";
|
||||||
|
|
||||||
|
interface PrivateRouteProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginContainer = styled.div`
|
||||||
|
${CenterHorizontally}
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
padding-top: 10%;
|
||||||
|
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LoginMenu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LoginMessage = styled.div`
|
||||||
|
${CenterHorizontally}
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
padding-bottom: 1em;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 500;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NewUserButton = styled(Button)`
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
/* border: 1px solid #b8e5fa; */
|
||||||
|
border-radius: 60px;
|
||||||
|
|
||||||
|
color: #0080ff;
|
||||||
|
background-color: #def8ff;
|
||||||
|
|
||||||
|
font-size: 1.1em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ExisitingUserButton = styled(Button)`
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
border-radius: 60px;
|
||||||
|
|
||||||
|
font-size: 1.1em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AuthGuard: FC<PrivateRouteProps> = ({ children }: PrivateRouteProps) => {
|
||||||
|
const { keycloak } = useKeycloak();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const [logging, setLogging] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const isLoggedIn = stage == "LOCAL" || keycloak.authenticated;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(updateUsername("tokenParsed" in keycloak ? keycloak.tokenParsed!.preferred_username : ""));
|
||||||
|
dispatch(updateFirstname("tokenParsed" in keycloak ? keycloak.tokenParsed!.given_name : ""));
|
||||||
|
dispatch(updateLastname("tokenParsed" in keycloak ? keycloak.tokenParsed!.family_name : ""));
|
||||||
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
const login = () => {
|
||||||
|
if (isLoggedIn) return;
|
||||||
|
keycloak.login();
|
||||||
|
setLogging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
keycloak.onAuthSuccess = function () {
|
||||||
|
setLogging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return isLoggedIn ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Header title={<PageHeader.Container>directory</PageHeader.Container>} noActions={true} />
|
||||||
|
|
||||||
|
{logging || isLoggedIn == undefined ? (
|
||||||
|
<RedirectPage />
|
||||||
|
) : (
|
||||||
|
<LoginContainer>
|
||||||
|
<LoginMenu>
|
||||||
|
<LoginMessage>Welcome! Please login to continue</LoginMessage>
|
||||||
|
<NewUserButton onClick={login}>New User (Coming Soon)</NewUserButton>
|
||||||
|
<ExisitingUserButton onClick={login}>Existing User</ExisitingUserButton>
|
||||||
|
</LoginMenu>
|
||||||
|
</LoginContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthGuard;
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
export const SpinnerOverlay = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Spinner = styled.div`
|
||||||
|
border: 5px solid #f3f3f3;
|
||||||
|
border-top: 5px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SpinnerLabel = styled.div`
|
||||||
|
color: white;
|
||||||
|
font-size: 2em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RedirectPage = ({}) => (
|
||||||
|
<SpinnerOverlay>
|
||||||
|
<Spinner />
|
||||||
|
<SpinnerLabel>Redirecting...</SpinnerLabel>
|
||||||
|
</SpinnerOverlay>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { LogoutOutlined, SettingsOutlined } from "@mui/icons-material";
|
||||||
|
import { useKeycloak } from "@react-keycloak/web";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import DropDown, { DropDownIconCSS } from "../../shared/basiccomponents/Dropdown/CustomDropDown";
|
||||||
|
import { selectFirstname, selectLastname } from "../../store/selectors/user.selector";
|
||||||
|
import { CenterVertically } from "../../utils/styles";
|
||||||
|
|
||||||
|
const UserCircle = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
background-color: rgb(243, 152, 78);
|
||||||
|
padding: 0.5em 0.8em;
|
||||||
|
border-radius: 100px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserBubble = () => {
|
||||||
|
const { keycloak } = useKeycloak();
|
||||||
|
// const username = useSelector(selectUsername);
|
||||||
|
const givenName = useSelector(selectFirstname);
|
||||||
|
const familyName = useSelector(selectLastname);
|
||||||
|
|
||||||
|
const firstInitial = givenName[0];
|
||||||
|
const lastInitial = familyName[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropDown
|
||||||
|
trigger={
|
||||||
|
<UserCircle>
|
||||||
|
{firstInitial}
|
||||||
|
{lastInitial}
|
||||||
|
</UserCircle>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
icon: <SettingsOutlined style={DropDownIconCSS} />,
|
||||||
|
label: <>Settings</>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <LogoutOutlined style={DropDownIconCSS} />,
|
||||||
|
label: <>Sign Out</>,
|
||||||
|
action: () => keycloak.logout()
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { AppDispatch } from "../../store";
|
||||||
|
import { selectMemberLoading, selectMembers } from "../../store/selectors/member.selectors";
|
||||||
|
import { Member } from "../../types/member";
|
||||||
|
import { Spinner, SpinnerLabel, SpinnerOverlay } from "../AuthGuard/Redirect";
|
||||||
|
|
||||||
|
const Directory: React.FC = () => {
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
const members: Dictionary<Member> = useSelector(selectMembers);
|
||||||
|
const isLoading: boolean = useSelector(selectMemberLoading) === "pending";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Directory</h1>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<SpinnerOverlay>
|
||||||
|
<Spinner />
|
||||||
|
<SpinnerLabel>Loading Members...</SpinnerLabel>
|
||||||
|
</SpinnerOverlay>
|
||||||
|
) : (
|
||||||
|
Object.values(members).map((member, index) => (
|
||||||
|
<h3 key={`h2-${member.firstName + member.lastName}-${index}`}>
|
||||||
|
{member.firstName + member.lastName}
|
||||||
|
</h3>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Directory;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
test("renders learn react link", () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||||
|
|
||||||
|
/* Projects */
|
||||||
|
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||||
|
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||||
|
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||||
|
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||||
|
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||||
|
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||||
|
|
||||||
|
/* Language and Environment */
|
||||||
|
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
|
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||||
|
"jsx": "preserve" /* Specify what JSX code is generated. */,
|
||||||
|
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||||
|
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||||
|
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||||
|
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||||
|
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||||
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
|
|
||||||
|
/* Modules */
|
||||||
|
"module": "commonjs" /* Specify what module code is generated. */,
|
||||||
|
"rootDir": "./" /* Specify the root folder within your source files. */,
|
||||||
|
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||||
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
|
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||||
|
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||||
|
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||||
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
|
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||||
|
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||||
|
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||||
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
|
/* JavaScript Support */
|
||||||
|
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||||
|
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||||
|
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||||
|
|
||||||
|
/* Emit */
|
||||||
|
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||||
|
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||||
|
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||||
|
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||||
|
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||||
|
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||||
|
"outDir": "build" /* Specify an output folder for all emitted files. */,
|
||||||
|
// "removeComments": true, /* Disable emitting comments. */
|
||||||
|
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||||
|
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||||
|
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||||
|
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||||
|
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||||
|
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||||
|
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||||
|
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||||
|
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||||
|
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||||
|
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||||
|
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||||
|
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||||
|
|
||||||
|
/* Interop Constraints */
|
||||||
|
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||||
|
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||||
|
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||||
|
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||||
|
|
||||||
|
/* Type Checking */
|
||||||
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
|
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||||
|
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||||
|
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||||
|
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||||
|
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||||
|
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||||
|
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||||
|
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||||
|
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||||
|
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||||
|
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||||
|
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||||
|
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||||
|
|
||||||
|
/* Completeness */
|
||||||
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue