Add client

This commit is contained in:
Steven Fan 2024-05-05 12:21:11 -04:00
parent d883bf185b
commit 5c47d7248d
91 changed files with 24914 additions and 0 deletions

1
client/.env Normal file
View File

@ -0,0 +1 @@
PORT=21287

23
client/.gitignore vendored Normal file
View File

@ -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*

31
client/Dockerfile Normal file
View File

@ -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

20
client/Dockerfile.Live Normal file
View File

@ -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" ]

80
client/README.md Normal file
View File

@ -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

20656
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

95
client/package.json Normal file
View File

@ -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"
}
}
}

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

39
client/public/index.html Normal file
View File

@ -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>

BIN
client/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
client/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -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"
}

3
client/public/robots.txt Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

4
client/src/App.css Normal file
View File

@ -0,0 +1,4 @@
.App {
text-align: center;
background-color: #282c34;
}

45
client/src/App.tsx Normal file
View File

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

18
client/src/colors.css Normal file
View File

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

26
client/src/index.css Normal file
View File

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

22
client/src/index.js Normal file
View File

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

11
client/src/keycloak.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&nbsp;</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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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%;
}

View File

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

View File

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

View File

@ -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;
`;

View File

@ -0,0 +1,9 @@
import styled from "styled-components";
import { CenterVertically, SpaceBetweenHorizontally } from "../../utils/styles";
export const Container = styled.div`
${CenterVertically}
${SpaceBetweenHorizontally}
width: 100%;
`;

View File

@ -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;
}
`
};

View File

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

View File

@ -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%;
`
};

View File

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

15
client/src/store/index.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
export default function remove<T>(list: T[], toRemove: T) {
return list.filter(item => item !== toRemove)
}

View File

@ -0,0 +1 @@
export const requestDelay = () => new Promise((resolve) => setTimeout(resolve, 500));

View File

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

View File

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

View File

@ -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.
}

View File

@ -0,0 +1,3 @@
export default function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

307
client/src/utils/styles.ts Normal file
View File

@ -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 */
`;

View File

@ -0,0 +1,7 @@
import { v4 as uuidv4 } from "uuid";
const uuidGenerator = () => {
return uuidv4();
};
export default uuidGenerator;

View File

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

View File

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

View File

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

View File

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

8
client/tests/App.test.js Normal file
View File

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

View File

@ -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";

109
client/tsconfig.json Normal file
View File

@ -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. */
}
}