Backup checkpoint one
This commit is contained in:
parent
164a60e336
commit
a1ad767261
|
|
@ -51,6 +51,7 @@
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-jss": "^10.10.0",
|
"react-jss": "^10.10.0",
|
||||||
|
"react-phone-input-2": "^2.15.1",
|
||||||
"react-redux": "^8.1.2",
|
"react-redux": "^8.1.2",
|
||||||
"react-router-dom": "^6.11.1",
|
"react-router-dom": "^6.11.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
|
@ -7587,6 +7588,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
|
||||||
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
|
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
|
||||||
|
},
|
||||||
"node_modules/clean-css": {
|
"node_modules/clean-css": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
|
||||||
|
|
@ -14437,11 +14443,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.reduce": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw=="
|
||||||
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||||
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="
|
"integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.startswith": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-XClYR1h4/fJ7H+mmCKppbiBmljN/nGs73iq2SjCT9SF4CBPoUHzLvWmH1GtZMhMBZSiRkHXfeA2RY1eIlJ75ww=="
|
||||||
|
},
|
||||||
"node_modules/lodash.uniq": {
|
"node_modules/lodash.uniq": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||||
|
|
@ -17049,6 +17065,23 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-phone-input-2": {
|
||||||
|
"version": "2.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-phone-input-2/-/react-phone-input-2-2.15.1.tgz",
|
||||||
|
"integrity": "sha512-W03abwhXcwUoq+vUFvC6ch2+LJYMN8qSOiO889UH6S7SyMCQvox/LF3QWt+cZagZrRdi5z2ON3omnjoCUmlaYw==",
|
||||||
|
"dependencies": {
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"lodash.memoize": "^4.1.2",
|
||||||
|
"lodash.reduce": "^4.6.0",
|
||||||
|
"lodash.startswith": "^4.2.1",
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0",
|
||||||
|
"react-dom": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "8.1.2",
|
"version": "8.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-jss": "^10.10.0",
|
"react-jss": "^10.10.0",
|
||||||
|
"react-phone-input-2": "^2.15.1",
|
||||||
"react-redux": "^8.1.2",
|
"react-redux": "^8.1.2",
|
||||||
"react-router-dom": "^6.11.1",
|
"react-router-dom": "^6.11.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,36 @@
|
||||||
import { ReactKeycloakProvider } from "@react-keycloak/web";
|
|
||||||
import { Chart as RegisterChart, registerables } from "chart.js";
|
import { Chart as RegisterChart, registerables } from "chart.js";
|
||||||
|
import { useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
import keycloak from "./keycloak";
|
|
||||||
import ToastDemo from "./shared/basiccomponents/Toast/Toast";
|
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";
|
import { AppDispatch } from "./store";
|
||||||
|
import { selectMemberLoading, selectMemberUpdating } from "./store/selectors/member.selectors";
|
||||||
|
import Directory from "./views/Directory/Directory";
|
||||||
|
|
||||||
export const DIRECTORY = "/";
|
export const DIRECTORY = "/";
|
||||||
|
|
||||||
RegisterChart.register(...registerables);
|
RegisterChart.register(...registerables);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [initialLoad, setInitialLoad] = useState<boolean>(true);
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const isLoading: boolean = useSelector(selectMemberLoading) === "pending";
|
const isLoading: boolean = useSelector(selectMemberLoading) === "pending";
|
||||||
const isUpdating: boolean = useSelector(selectMemberUpdating) === "pending";
|
const isUpdating: boolean = useSelector(selectMemberUpdating) === "pending";
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(memberActions.fetchMembers());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReactKeycloakProvider authClient={keycloak}>
|
{/* <ReactKeycloakProvider authClient={keycloak}> */}
|
||||||
<ToastDemo isOpen={isLoading} text="Loading members" />
|
{/* <AuthGuard> */}
|
||||||
<ToastDemo isOpen={isUpdating} text="Saving member" />
|
|
||||||
<AuthGuard>
|
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={DIRECTORY} Component={Directory} />
|
<Route path={DIRECTORY} Component={Directory} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthGuard>
|
{/* </AuthGuard> */}
|
||||||
</ReactKeycloakProvider>
|
{/* </ReactKeycloakProvider> */}
|
||||||
|
<ToastDemo isOpen={isLoading} text="Loading members" />
|
||||||
|
<ToastDemo isOpen={isUpdating} text="Saving member" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
declare module '*.jpg';
|
||||||
|
declare module '*.png';
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import Keycloak from "keycloak-js";
|
// import Keycloak from "keycloak-js";
|
||||||
|
|
||||||
const KeycloakURL = "https://directory.dojo1.e3labs.net/"
|
// const KeycloakURL = ""
|
||||||
|
|
||||||
const keycloak = new Keycloak({
|
// const keycloak = new Keycloak({
|
||||||
url: KeycloakURL,
|
// url: KeycloakURL,
|
||||||
realm: "directory",
|
// realm: "directory",
|
||||||
clientId: "React-auth",
|
// clientId: "React-auth",
|
||||||
});
|
// });
|
||||||
|
|
||||||
export default keycloak;
|
// export default keycloak;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface CreateFormProps<Document> {
|
||||||
isDocument: (object: any) => object is Document;
|
isDocument: (object: any) => object is Document;
|
||||||
formInputs: formInput[];
|
formInputs: formInput[];
|
||||||
triggerElement: JSX.Element;
|
triggerElement: JSX.Element;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateFormState = {};
|
type CreateFormState = {};
|
||||||
|
|
@ -29,13 +30,13 @@ export default class CreateForm<Document> extends React.Component<CreateFormProp
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// console.log("render Secondary Service: " + JSON.stringify(this.state, null, 4))
|
// console.log("render Secondary Service: " + JSON.stringify(this.state, null, 4))
|
||||||
const { formInputs, isModalOpen, onCloseModal, triggerElement } = this.props;
|
const { formInputs, isModalOpen, onCloseModal, triggerElement, title } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal<Document>
|
<Modal<Document>
|
||||||
onSubmitForm={this.onFormSubmit}
|
onSubmitForm={this.onFormSubmit}
|
||||||
triggerElement={triggerElement}
|
triggerElement={triggerElement}
|
||||||
modalTitle={""}
|
modalTitle={title || ""}
|
||||||
modalDescription=""
|
modalDescription=""
|
||||||
submitFormButtonText="Save"
|
submitFormButtonText="Save"
|
||||||
formInputs={formInputs}
|
formInputs={formInputs}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Dictionary } from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SPACE_BETWEEN_HORIZONTALLY } from "../../../utils/styles";
|
import { SPACE_BETWEEN_HORIZONTALLY } from "../../../utils/styles";
|
||||||
import Select from "../Select/Select";
|
import Select from "../Select/Select";
|
||||||
|
import PhoneInput from "react-phone-input-2";
|
||||||
|
|
||||||
export type formInput = {
|
export type formInput = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -9,6 +10,7 @@ export type formInput = {
|
||||||
validationMessage: string;
|
validationMessage: string;
|
||||||
inputProps: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
inputProps: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
|
||||||
options?: Dictionary<string>;
|
options?: Dictionary<string>;
|
||||||
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormProps = {
|
export type FormProps = {
|
||||||
|
|
@ -84,7 +86,7 @@ export default class Form extends React.Component<FormProps, FormState> {
|
||||||
<form className="FormRoot" onSubmit={this.onSubmit}>
|
<form className="FormRoot" onSubmit={this.onSubmit}>
|
||||||
{formInputs.map((formInput) => {
|
{formInputs.map((formInput) => {
|
||||||
return (
|
return (
|
||||||
<div key={formInput.name} className="FormField">
|
<div key={formInput.name} className="FormField" style={formInput.style}>
|
||||||
{/* Range Input additional label */}
|
{/* Range Input additional label */}
|
||||||
{formInput.inputProps.type && formInput.inputProps.type === "range" ? (
|
{formInput.inputProps.type && formInput.inputProps.type === "range" ? (
|
||||||
<label className="FormLabel">
|
<label className="FormLabel">
|
||||||
|
|
@ -110,7 +112,16 @@ export default class Form extends React.Component<FormProps, FormState> {
|
||||||
const fields = this.state.fields;
|
const fields = this.state.fields;
|
||||||
this.setState({ fields: { ...fields, ...{ [formInput.name]: value } } });
|
this.setState({ fields: { ...fields, ...{ [formInput.name]: value } } });
|
||||||
}}
|
}}
|
||||||
triggerStyle={{ ...SPACE_BETWEEN_HORIZONTALLY, width: "100%", }}
|
triggerStyle={{ ...SPACE_BETWEEN_HORIZONTALLY, width: "100%" }}
|
||||||
|
/>
|
||||||
|
) : formInput.inputProps.type == "tel" ? (
|
||||||
|
<PhoneInput
|
||||||
|
inputClass="Input"
|
||||||
|
placeholder="(123) 456-7890"
|
||||||
|
onChange={(value, data, event) => this.handleFormInput(event)}
|
||||||
|
specialLabel=""
|
||||||
|
disableCountryCode={true}
|
||||||
|
inputProps={{ name: formInput.name }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,16 @@ input {
|
||||||
}
|
}
|
||||||
|
|
||||||
.FormRoot {
|
.FormRoot {
|
||||||
width: 260px;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FormField {
|
.FormField {
|
||||||
display: grid;
|
flex: 1;
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.FormLabel {
|
.FormLabel {
|
||||||
|
|
@ -50,11 +54,13 @@ input {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 90vw;
|
min-width: 30vw;
|
||||||
max-width: 450px;
|
max-width: 90vw;
|
||||||
max-height: 100vh;
|
max-height: 80vh;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.DialogContent:focus {
|
.DialogContent:focus {
|
||||||
|
|
@ -165,7 +171,7 @@ input {
|
||||||
|
|
||||||
.Label {
|
.Label {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: var(--violet11);
|
/* color: var(--violet11); */
|
||||||
width: 90px;
|
width: 90px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import * as RadixSelect from "@radix-ui/react-select";
|
||||||
import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons";
|
import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons";
|
||||||
import "./styles.css";
|
import "./styles.css";
|
||||||
import { Dictionary } from "lodash";
|
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.
|
Make sure the values are rendered the first time! Changing the values leads to weird behaviors.
|
||||||
|
|
@ -42,16 +41,16 @@ export default class Select extends React.Component<SelectProps, SelectState> {
|
||||||
return this.props.hidden ? (
|
return this.props.hidden ? (
|
||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<GraphOption.Select>
|
<>
|
||||||
<RadixSelect.Root onValueChange={this.onValueChange} defaultValue={this.props.defaultValue + ""}>
|
<RadixSelect.Root onValueChange={this.onValueChange} defaultValue={this.props.defaultValue + ""}>
|
||||||
<RadixSelect.Trigger className={""} style={this.props.triggerStyle} aria-label="Food">
|
<RadixSelect.Trigger className={"Input"} style={this.props.triggerStyle} aria-label="Food">
|
||||||
<RadixSelect.Value placeholder={this.props.triggerValue} />
|
<RadixSelect.Value placeholder={this.props.triggerValue} />
|
||||||
<RadixSelect.Icon className="SelectIcon">
|
<RadixSelect.Icon className="SelectIcon">
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
</RadixSelect.Icon>
|
</RadixSelect.Icon>
|
||||||
</RadixSelect.Trigger>
|
</RadixSelect.Trigger>
|
||||||
<RadixSelect.Portal>
|
<RadixSelect.Portal>
|
||||||
<RadixSelect.Content className="SelectContent">
|
<RadixSelect.Content>
|
||||||
<RadixSelect.Viewport className="SelectViewport">
|
<RadixSelect.Viewport className="SelectViewport">
|
||||||
<RadixSelect.Group>
|
<RadixSelect.Group>
|
||||||
<RadixSelect.Label className="SelectLabel">{this.props.label}</RadixSelect.Label>
|
<RadixSelect.Label className="SelectLabel">{this.props.label}</RadixSelect.Label>
|
||||||
|
|
@ -71,7 +70,7 @@ export default class Select extends React.Component<SelectProps, SelectState> {
|
||||||
</RadixSelect.Content>
|
</RadixSelect.Content>
|
||||||
</RadixSelect.Portal>
|
</RadixSelect.Portal>
|
||||||
</RadixSelect.Root>
|
</RadixSelect.Root>
|
||||||
</GraphOption.Select>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,15 @@ button {
|
||||||
width: 8em;
|
width: 8em;
|
||||||
box-shadow: 0 2px 10px var(--blackA7);
|
box-shadow: 0 2px 10px var(--blackA7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.SelectTrigger:hover {
|
.SelectTrigger:hover {
|
||||||
background-color: var(--mauve3);
|
background-color: var(--mauve3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.SelectTrigger:focus {
|
.SelectTrigger:focus {
|
||||||
box-shadow: 0 0 0 2px black;
|
box-shadow: 0 0 0 2px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SelectTrigger[data-placeholder] {
|
.SelectTrigger[data-placeholder] {
|
||||||
color: var(--violet9);
|
color: var(--violet9);
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +40,6 @@ button {
|
||||||
|
|
||||||
.SelectContent {
|
.SelectContent {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: white;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
|
@ -46,6 +48,10 @@ button {
|
||||||
|
|
||||||
.SelectViewport {
|
.SelectViewport {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow:
|
||||||
|
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
|
||||||
|
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.SelectItem {
|
.SelectItem {
|
||||||
|
|
@ -60,10 +66,12 @@ button {
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SelectItem[data-disabled] {
|
.SelectItem[data-disabled] {
|
||||||
color: var(--mauve8);
|
color: var(--mauve8);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SelectItem[data-highlighted] {
|
.SelectItem[data-highlighted] {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: var(--violet9);
|
background-color: var(--violet9);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Dictionary } from "lodash";
|
import { Dictionary } from "lodash";
|
||||||
|
|
||||||
const envToRemoteMapping: Dictionary<string> = {
|
const envToRemoteMapping: Dictionary<string> = {
|
||||||
LOCAL: "",
|
LOCAL: "http://localhost:21288/",
|
||||||
DEV: "",
|
DEV: "",
|
||||||
STAGE: "",
|
STAGE: "",
|
||||||
PROD: ""
|
PROD: ""
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,6 @@ const membersSlice = createSlice({
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.addCase(memberActions.fetchMembers.fulfilled, (state, action) => {
|
.addCase(memberActions.fetchMembers.fulfilled, (state, action) => {
|
||||||
console.log("fetchMembers action recieved: " + JSON.stringify(action.payload, null, 4));
|
|
||||||
|
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
loading: "succeeded",
|
loading: "succeeded",
|
||||||
members: couchDocumentListToDictionary(action.payload)
|
members: couchDocumentListToDictionary(action.payload)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { couchDocument } from "./CouchDocument";
|
||||||
export type Member = couchDocument & {
|
export type Member = couchDocument & {
|
||||||
apt: string;
|
apt: string;
|
||||||
birthday: string;
|
birthday: string;
|
||||||
|
birthmonth?: string;
|
||||||
city: string;
|
city: string;
|
||||||
communityGroup: string;
|
communityGroup: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -30,6 +31,7 @@ export const MemberUtils = {
|
||||||
_id: uuidGenerator(),
|
_id: uuidGenerator(),
|
||||||
apt: "",
|
apt: "",
|
||||||
birthday: "",
|
birthday: "",
|
||||||
|
birthmonth: "",
|
||||||
city: "",
|
city: "",
|
||||||
communityGroup: "",
|
communityGroup: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
|
@ -48,19 +50,244 @@ export const MemberUtils = {
|
||||||
streetType: "",
|
streetType: "",
|
||||||
userLogin: "",
|
userLogin: "",
|
||||||
zipCode: "",
|
zipCode: "",
|
||||||
// image: ""
|
image: ""
|
||||||
}),
|
}),
|
||||||
|
|
||||||
validate: (object: any): object is Member => "_id" in object,
|
validate: (object: any): object is Member => "_id" in object,
|
||||||
|
|
||||||
inputs: (Member?: Member) => ({
|
inputs: (Member?: Member) => ({
|
||||||
name: {
|
apt: {
|
||||||
name: "name",
|
name: "apt",
|
||||||
label: "Member Name",
|
label: "Apt #",
|
||||||
validationMessage: "Please enter the name of the Member",
|
validationMessage: "Leave blank space if none.",
|
||||||
inputProps: {
|
inputProps: {
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: Member?.name
|
defaultValue: Member?.apt
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "10%",
|
||||||
|
maxWidth: "10%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
birthday: {
|
||||||
|
name: "birthday",
|
||||||
|
label: "Birth Day",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.birthday,
|
||||||
|
type: "month"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
birthmonth: {
|
||||||
|
name: "birthmonth",
|
||||||
|
label: "Birth Month",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.birthday,
|
||||||
|
type: "day"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
name: "city",
|
||||||
|
label: "City",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.city
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "40%",
|
||||||
|
maxWidth: "40%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
communityGroup: {
|
||||||
|
name: "communityGroup",
|
||||||
|
label: "Community Group",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.communityGroup
|
||||||
|
}
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
name: "email",
|
||||||
|
label: "Email",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.email,
|
||||||
|
type: "email"
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "65%",
|
||||||
|
maxWidth: "65%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
firstName: {
|
||||||
|
name: "firstName",
|
||||||
|
label: "First Name",
|
||||||
|
validationMessage: "Please enter the first name",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.firstName
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "40%",
|
||||||
|
maxWidth: "40%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
houseNumber: {
|
||||||
|
name: "houseNumber",
|
||||||
|
label: "House #",
|
||||||
|
validationMessage: "Please enter the house number",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.houseNumber
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "11%",
|
||||||
|
maxWidth: "11%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
name: "lastName",
|
||||||
|
label: "Last Name",
|
||||||
|
validationMessage: "Please enter the last name",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.lastName
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "40%",
|
||||||
|
maxWidth: "40%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
marriedTo: {
|
||||||
|
name: "marriedTo",
|
||||||
|
label: "Spouse Name",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.marriedTo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spouseIsMember: {
|
||||||
|
name: "spouseIsMember",
|
||||||
|
label: "Is spouse a member?",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.spouseIsMember
|
||||||
|
}
|
||||||
|
},
|
||||||
|
memberSince: {
|
||||||
|
name: "memberSince",
|
||||||
|
label: "Member since",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.memberSince
|
||||||
|
}
|
||||||
|
},
|
||||||
|
memberStatus: {
|
||||||
|
name: "memberStatus",
|
||||||
|
label: "Member Status",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.memberStatus
|
||||||
|
}
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
name: "phone",
|
||||||
|
label: "Phone #",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.phone,
|
||||||
|
type: "tel"
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "25%",
|
||||||
|
maxWidth: "25%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// pictureFilename: {
|
||||||
|
// name: "pictureFilename",
|
||||||
|
// label: "Name of picture",
|
||||||
|
// validationMessage: "Leave blank space if none.",
|
||||||
|
// inputProps: {
|
||||||
|
// required: true,
|
||||||
|
// defaultValue: Member?.pictureFilename
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
roles: {
|
||||||
|
name: "roles",
|
||||||
|
label: "Member roles",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.roles
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
name: "state",
|
||||||
|
label: "State",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.state
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
MD: "Maryland",
|
||||||
|
VA: "Virginia",
|
||||||
|
DC: "DC"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
streetName: {
|
||||||
|
name: "streetName",
|
||||||
|
label: "Street Name",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.streetName
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "50%",
|
||||||
|
maxWidth: "50%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
streetType: {
|
||||||
|
name: "streetType",
|
||||||
|
label: "Type",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.streetType
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
minWidth: "10%",
|
||||||
|
maxWidth: "10%"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
userLogin: {
|
||||||
|
name: "userLogin",
|
||||||
|
label: "User login",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.userLogin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zipCode: {
|
||||||
|
name: "zipCode",
|
||||||
|
label: "Zipcode",
|
||||||
|
validationMessage: "Leave blank space if none.",
|
||||||
|
inputProps: {
|
||||||
|
required: true,
|
||||||
|
defaultValue: Member?.zipCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,117 @@
|
||||||
import { useKeycloak } from "@react-keycloak/web";
|
// import { useKeycloak } from "@react-keycloak/web";
|
||||||
import { FC, ReactNode, useEffect, useState } from "react";
|
// import { FC, ReactNode, useEffect, useState } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
// import { useDispatch } from "react-redux";
|
||||||
import styled from "styled-components";
|
// import styled from "styled-components";
|
||||||
import Header from "../../shared/basiccomponents/Header/Header";
|
// import Header from "../../shared/basiccomponents/Header/Header";
|
||||||
import { stage } from "../../shared/couchconnector/CouchCrudService";
|
// import { stage } from "../../shared/couchconnector/CouchCrudService";
|
||||||
import { Button } from "../../shared/styledcomponents/Button";
|
// import { Button } from "../../shared/styledcomponents/Button";
|
||||||
import { PageHeader } from "../../shared/styledcomponents/PageHeader";
|
// import { PageHeader } from "../../shared/styledcomponents/PageHeader";
|
||||||
import { updateFirstname, updateLastname, updateUsername } from "../../store/reducers/user.reducer";
|
// import { updateFirstname, updateLastname, updateUsername } from "../../store/reducers/user.reducer";
|
||||||
import { CenterHorizontally, CenterVertically } from "../../utils/styles";
|
// import { CenterHorizontally, CenterVertically } from "../../utils/styles";
|
||||||
import { RedirectPage } from "./Redirect";
|
// import { RedirectPage } from "./Redirect";
|
||||||
|
|
||||||
interface PrivateRouteProps {
|
// interface PrivateRouteProps {
|
||||||
children?: ReactNode;
|
// children?: ReactNode;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const LoginContainer = styled.div`
|
// const LoginContainer = styled.div`
|
||||||
${CenterHorizontally}
|
// ${CenterHorizontally}
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
height: 100vh;
|
// height: 100vh;
|
||||||
|
|
||||||
padding-top: 10%;
|
// padding-top: 10%;
|
||||||
|
|
||||||
border: 1px solid #ccc;
|
// border: 1px solid #ccc;
|
||||||
border-radius: 8px;
|
// border-radius: 8px;
|
||||||
background-color: #fff;
|
// background-color: #fff;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
// box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
`;
|
// `;
|
||||||
|
|
||||||
const LoginMenu = styled.div`
|
// const LoginMenu = styled.div`
|
||||||
display: flex;
|
// display: flex;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
gap: 0.5em;
|
// gap: 0.5em;
|
||||||
`;
|
// `;
|
||||||
|
|
||||||
const LoginMessage = styled.div`
|
// const LoginMessage = styled.div`
|
||||||
${CenterHorizontally}
|
// ${CenterHorizontally}
|
||||||
${CenterVertically}
|
// ${CenterVertically}
|
||||||
|
|
||||||
padding-bottom: 1em;
|
// padding-bottom: 1em;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
font-size: 2em;
|
// font-size: 2em;
|
||||||
font-weight: 500;
|
// font-weight: 500;
|
||||||
`;
|
// `;
|
||||||
|
|
||||||
const NewUserButton = styled(Button)`
|
// const NewUserButton = styled(Button)`
|
||||||
width: 80%;
|
// width: 80%;
|
||||||
margin: 0 auto;
|
// margin: 0 auto;
|
||||||
|
|
||||||
padding: 1em;
|
// padding: 1em;
|
||||||
|
|
||||||
/* border: 1px solid #b8e5fa; */
|
// /* border: 1px solid #b8e5fa; */
|
||||||
border-radius: 60px;
|
// border-radius: 60px;
|
||||||
|
|
||||||
color: #0080ff;
|
// color: #0080ff;
|
||||||
background-color: #def8ff;
|
// background-color: #def8ff;
|
||||||
|
|
||||||
font-size: 1.1em;
|
// font-size: 1.1em;
|
||||||
`;
|
// `;
|
||||||
|
|
||||||
const ExisitingUserButton = styled(Button)`
|
// const ExisitingUserButton = styled(Button)`
|
||||||
width: 80%;
|
// width: 80%;
|
||||||
margin: 0 auto;
|
// margin: 0 auto;
|
||||||
|
|
||||||
padding: 1em;
|
// padding: 1em;
|
||||||
|
|
||||||
border-radius: 60px;
|
// border-radius: 60px;
|
||||||
|
|
||||||
font-size: 1.1em;
|
// font-size: 1.1em;
|
||||||
`;
|
// `;
|
||||||
|
|
||||||
const AuthGuard: FC<PrivateRouteProps> = ({ children }: PrivateRouteProps) => {
|
// const AuthGuard: FC<PrivateRouteProps> = ({ children }: PrivateRouteProps) => {
|
||||||
const { keycloak } = useKeycloak();
|
// const { keycloak } = useKeycloak();
|
||||||
const dispatch = useDispatch();
|
// const dispatch = useDispatch();
|
||||||
|
|
||||||
const [logging, setLogging] = useState<boolean>(false);
|
// const [logging, setLogging] = useState<boolean>(false);
|
||||||
|
|
||||||
const isLoggedIn = stage == "LOCAL" || keycloak.authenticated;
|
// const isLoggedIn = stage == "LOCAL" || keycloak.authenticated;
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
dispatch(updateUsername("tokenParsed" in keycloak ? keycloak.tokenParsed!.preferred_username : ""));
|
// dispatch(updateUsername("tokenParsed" in keycloak ? keycloak.tokenParsed!.preferred_username : ""));
|
||||||
dispatch(updateFirstname("tokenParsed" in keycloak ? keycloak.tokenParsed!.given_name : ""));
|
// dispatch(updateFirstname("tokenParsed" in keycloak ? keycloak.tokenParsed!.given_name : ""));
|
||||||
dispatch(updateLastname("tokenParsed" in keycloak ? keycloak.tokenParsed!.family_name : ""));
|
// dispatch(updateLastname("tokenParsed" in keycloak ? keycloak.tokenParsed!.family_name : ""));
|
||||||
}, [isLoggedIn]);
|
// }, [isLoggedIn]);
|
||||||
|
|
||||||
const login = () => {
|
// const login = () => {
|
||||||
if (isLoggedIn) return;
|
// if (isLoggedIn) return;
|
||||||
keycloak.login();
|
// keycloak.login();
|
||||||
setLogging(true);
|
// setLogging(true);
|
||||||
};
|
// };
|
||||||
|
|
||||||
keycloak.onAuthSuccess = function () {
|
// keycloak.onAuthSuccess = function () {
|
||||||
setLogging(false);
|
// setLogging(false);
|
||||||
};
|
// };
|
||||||
|
|
||||||
return isLoggedIn ? (
|
// return isLoggedIn ? (
|
||||||
children
|
// children
|
||||||
) : (
|
// ) : (
|
||||||
<>
|
// <>
|
||||||
<Header title={<PageHeader.Container>directory</PageHeader.Container>} noActions={true} />
|
// <Header title={<PageHeader.Container>directory</PageHeader.Container>} noActions={true} />
|
||||||
|
|
||||||
{logging || isLoggedIn == undefined ? (
|
// {logging || isLoggedIn == undefined ? (
|
||||||
<RedirectPage />
|
// <RedirectPage />
|
||||||
) : (
|
// ) : (
|
||||||
<LoginContainer>
|
// <LoginContainer>
|
||||||
<LoginMenu>
|
// <LoginMenu>
|
||||||
<LoginMessage>Welcome! Please login to continue</LoginMessage>
|
// <LoginMessage>Welcome! Please login to continue</LoginMessage>
|
||||||
<NewUserButton onClick={login}>New User (Coming Soon)</NewUserButton>
|
// <NewUserButton onClick={login}>New User (Coming Soon)</NewUserButton>
|
||||||
<ExisitingUserButton onClick={login}>Existing User</ExisitingUserButton>
|
// <ExisitingUserButton onClick={login}>Existing User</ExisitingUserButton>
|
||||||
</LoginMenu>
|
// </LoginMenu>
|
||||||
</LoginContainer>
|
// </LoginContainer>
|
||||||
)}
|
// )}
|
||||||
</>
|
// </>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
export default AuthGuard;
|
// export default AuthGuard;
|
||||||
|
|
|
||||||
|
|
@ -2,30 +2,171 @@ import { Dictionary } from "lodash";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { AppDispatch } from "../../store";
|
import { AppDispatch } from "../../store";
|
||||||
import { selectMemberLoading, selectMembers } from "../../store/selectors/member.selectors";
|
import { selectMemberLoading, selectMembers } from "../../store/selectors/member.selectors";
|
||||||
import { Member } from "../../types/member";
|
import { Member, MemberUtils } from "../../types/member";
|
||||||
import { Spinner, SpinnerLabel, SpinnerOverlay } from "../AuthGuard/Redirect";
|
import { Spinner, SpinnerLabel, SpinnerOverlay } from "../AuthGuard/Redirect";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { memberActions } from "../../store/actions/member.actions";
|
||||||
|
import ListViewRow from "./ListViewRow";
|
||||||
|
import { Delete, Edit, WysiwygRounded } from "@mui/icons-material";
|
||||||
|
import { DropDownIconCSS } from "../../shared/basiccomponents/Dropdown/CustomDropDown";
|
||||||
|
import { BorderedContainer, CenterVertically, SpaceBetweenHorizontally, StackFromBottom } from "../../utils/styles";
|
||||||
|
import MemberPhoto from "./MemberPhoto";
|
||||||
|
import Logo from "../Directory/ncbc_white.png";
|
||||||
|
import { PlusIcon } from "@radix-ui/react-icons";
|
||||||
|
import CreateForm from "../../shared/basiccomponents/FormModal/CreateForm";
|
||||||
|
import ToggableTooltip from "../../shared/basiccomponents/InfoToolTip/ToggableToolTip";
|
||||||
|
import DeleteForm from "../../shared/basiccomponents/FormModal/DeleteForm";
|
||||||
|
import MemberRow from "./MemberRow";
|
||||||
|
import { Button } from "../../shared/styledcomponents/Button";
|
||||||
|
|
||||||
|
export const NCBCBlue = "#244A7F";
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
gap: 1em;
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${NCBCBlue};
|
||||||
|
color: white;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Table = styled.div`
|
||||||
|
${BorderedContainer}
|
||||||
|
width: 80%;
|
||||||
|
margin: 1em auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const logoSize = "1em";
|
||||||
|
|
||||||
|
const LogoContainer = styled.img`
|
||||||
|
height: ${logoSize};
|
||||||
|
width: ${logoSize};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CreateButton = {
|
||||||
|
Container: styled.div`
|
||||||
|
${StackFromBottom}
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5em;
|
||||||
|
right: 1.5em;
|
||||||
|
`,
|
||||||
|
|
||||||
|
Trigger: styled.div`
|
||||||
|
box-shadow:
|
||||||
|
0px 6px 10px 0px rgba(216, 213, 213, 0.14),
|
||||||
|
0px 1px 18px 0px rgba(0, 0, 0, 0.12),
|
||||||
|
0px 3px 5px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 60px;
|
||||||
|
padding: 0.5em;
|
||||||
|
`,
|
||||||
|
|
||||||
|
Icon: styled(PlusIcon)`
|
||||||
|
height: 3em;
|
||||||
|
width: 3em;
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
const memberFormInputs = MemberUtils.inputs();
|
||||||
|
|
||||||
|
const createMemberInputs = [
|
||||||
|
memberFormInputs.firstName,
|
||||||
|
memberFormInputs.lastName,
|
||||||
|
memberFormInputs.phone,
|
||||||
|
memberFormInputs.email,
|
||||||
|
memberFormInputs.houseNumber,
|
||||||
|
memberFormInputs.streetName,
|
||||||
|
memberFormInputs.streetType,
|
||||||
|
memberFormInputs.apt,
|
||||||
|
memberFormInputs.city,
|
||||||
|
memberFormInputs.state,
|
||||||
|
memberFormInputs.zipCode,
|
||||||
|
memberFormInputs.birthday,
|
||||||
|
memberFormInputs.marriedTo,
|
||||||
|
memberFormInputs.spouseIsMember,
|
||||||
|
memberFormInputs.memberSince,
|
||||||
|
memberFormInputs.communityGroup,
|
||||||
|
memberFormInputs.memberStatus,
|
||||||
|
memberFormInputs.roles
|
||||||
|
];
|
||||||
|
|
||||||
const Directory: React.FC = () => {
|
const Directory: React.FC = () => {
|
||||||
|
const [initialLoad, setInitialLoad] = useState<boolean>(true);
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
||||||
|
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const dispatch = useDispatch<AppDispatch>();
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
const members: Dictionary<Member> = useSelector(selectMembers);
|
const members: Dictionary<Member> = useSelector(selectMembers);
|
||||||
|
useEffect(() => {
|
||||||
|
initialLoad && dispatch(memberActions.fetchMembers());
|
||||||
|
setInitialLoad(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isLoading: boolean = useSelector(selectMemberLoading) === "pending";
|
const isLoading: boolean = useSelector(selectMemberLoading) === "pending";
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const currentMonth = currentDate.toLocaleString("default", { month: "long" });
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
const customAction = () => {
|
||||||
|
// Object.entries(members).forEach(([id, member]) => {
|
||||||
|
// dispatch(
|
||||||
|
// memberActions.updateMember({
|
||||||
|
// ...member,
|
||||||
|
// birthmonth: member.birthday.split(" ")[0],
|
||||||
|
// birthday: member.birthday.split(" ")[1]
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Directory</h1>
|
<Title>
|
||||||
|
<LogoContainer src={Logo} />
|
||||||
|
NCBC Members Directory ({currentMonth} {currentYear} Edition)
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{/* <Button onClick={customAction}>Custom Action</Button> */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SpinnerOverlay>
|
<SpinnerOverlay>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<SpinnerLabel>Loading Members...</SpinnerLabel>
|
<SpinnerLabel>Loading Members...</SpinnerLabel>
|
||||||
</SpinnerOverlay>
|
</SpinnerOverlay>
|
||||||
) : (
|
) : (
|
||||||
Object.values(members).map((member, index) => (
|
<Table>
|
||||||
<h3 key={`h2-${member.firstName + member.lastName}-${index}`}>
|
{Object.values(members)
|
||||||
{member.firstName + member.lastName}
|
.sort((a, b) => (a.lastName + a.firstName).localeCompare(b.lastName + b.firstName))
|
||||||
</h3>
|
.map((member) => (
|
||||||
))
|
<MemberRow member={member} />
|
||||||
|
))}
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
{!isLoading && (
|
||||||
|
<CreateButton.Container>
|
||||||
|
<ToggableTooltip
|
||||||
|
content={"Add New Member"}
|
||||||
|
trigger={
|
||||||
|
<CreateForm<Member>
|
||||||
|
document={MemberUtils.getDefault()}
|
||||||
|
isDocument={MemberUtils.validate}
|
||||||
|
formInputs={createMemberInputs}
|
||||||
|
onSubmitForm={async (member: Member) => dispatch(memberActions.createMember(member))}
|
||||||
|
triggerElement={
|
||||||
|
<CreateButton.Trigger>
|
||||||
|
<CreateButton.Icon />
|
||||||
|
</CreateButton.Trigger>
|
||||||
|
}
|
||||||
|
title="New Member"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CreateButton.Container>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import ClickOutsideDetector from "../../shared/basiccomponents/ClickOutsideDetector/ClickOutsideDetector";
|
||||||
|
import DropDown, { action } from "../../shared/basiccomponents/Dropdown/CustomDropDown";
|
||||||
|
import { BorderRow, HoverableRow, CenterVertically, SpaceBetweenHorizontally } from "../../utils/styles";
|
||||||
|
|
||||||
|
interface ListViewRowContainerProps {
|
||||||
|
hoverable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListViewRowContainer = styled.div<ListViewRowContainerProps>`
|
||||||
|
${BorderRow}
|
||||||
|
${({ hoverable }) => hoverable && HoverableRow}
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
padding: 1em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconContainer = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
|
||||||
|
gap: 1em;
|
||||||
|
width: 50%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ActionsContainer = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
${SpaceBetweenHorizontally}
|
||||||
|
|
||||||
|
padding-right: 15px;
|
||||||
|
width: 50%;
|
||||||
|
text-align: right;
|
||||||
|
gap: 1em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DropdownMenuWrapper = styled.div`
|
||||||
|
${CenterVertically}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ListViewRowProps {
|
||||||
|
key: string;
|
||||||
|
hoverable: boolean;
|
||||||
|
onClick: VoidFunction;
|
||||||
|
icon: JSX.Element;
|
||||||
|
iconlabel: string;
|
||||||
|
invisibleActions: JSX.Element;
|
||||||
|
actions: JSX.Element;
|
||||||
|
dropdownActions: action[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListViewRow: React.FC<ListViewRowProps> = ({
|
||||||
|
key,
|
||||||
|
hoverable,
|
||||||
|
onClick,
|
||||||
|
icon,
|
||||||
|
iconlabel,
|
||||||
|
invisibleActions,
|
||||||
|
actions,
|
||||||
|
dropdownActions
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||||
|
const [showMenu, setShowMenu] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListViewRowContainer
|
||||||
|
key={key}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
hoverable={hoverable}
|
||||||
|
onContextMenu={(event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setShowMenu(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Icon + Name */}
|
||||||
|
<IconContainer>
|
||||||
|
{icon}
|
||||||
|
{iconlabel}
|
||||||
|
</IconContainer>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<ActionsContainer>
|
||||||
|
{/* Padding empty items */}
|
||||||
|
<div
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{invisibleActions}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
{actions}
|
||||||
|
</ActionsContainer>
|
||||||
|
|
||||||
|
{/* Dropdown */}
|
||||||
|
<ClickOutsideDetector onClickOutside={() => setShowMenu(false)}>
|
||||||
|
<DropdownMenuWrapper
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropDown isOpen={showMenu} hidden={!isHovered} actions={dropdownActions} />
|
||||||
|
</DropdownMenuWrapper>
|
||||||
|
</ClickOutsideDetector>
|
||||||
|
</ListViewRowContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListViewRow;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const imageSize = "3em";
|
||||||
|
const ImageContainer = styled.img`
|
||||||
|
height: ${imageSize};
|
||||||
|
width: ${imageSize};
|
||||||
|
border-radius: ${imageSize};
|
||||||
|
`
|
||||||
|
const MemberPhoto: React.FC<{ base64String: string }> = ({ base64String }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ImageContainer src={"data:image/jpeg;base64," + base64String} alt="Image" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberPhoto;
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Delete, Edit } from "@mui/icons-material";
|
||||||
|
import { Dictionary } from "lodash";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { DropDownIconCSS } from "../../shared/basiccomponents/Dropdown/CustomDropDown";
|
||||||
|
import { AppDispatch } from "../../store";
|
||||||
|
import { selectMembers } from "../../store/selectors/member.selectors";
|
||||||
|
import { Member, MemberUtils } from "../../types/member";
|
||||||
|
import ListViewRow from "./ListViewRow";
|
||||||
|
import MemberPhoto from "./MemberPhoto";
|
||||||
|
import DeleteForm from "../../shared/basiccomponents/FormModal/DeleteForm";
|
||||||
|
import { memberActions } from "../../store/actions/member.actions";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
const Cell = styled.div``;
|
||||||
|
|
||||||
|
interface MemberRowProps {
|
||||||
|
member: Member;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MemberRow: React.FC<MemberRowProps> = ({ member }: MemberRowProps) => {
|
||||||
|
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
|
||||||
|
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<AppDispatch>();
|
||||||
|
|
||||||
|
const members: Dictionary<Member> = useSelector(selectMembers);
|
||||||
|
|
||||||
|
const DeleteMemberForm = ({ member }: { member: Member }) => (
|
||||||
|
<DeleteForm<Member>
|
||||||
|
document={member}
|
||||||
|
documentName={member.name}
|
||||||
|
isDocument={MemberUtils.validate}
|
||||||
|
formInputs={[MemberUtils.inputs(member).firstName]}
|
||||||
|
onSubmitForm={() => {
|
||||||
|
dispatch(memberActions.deleteMember(member._id));
|
||||||
|
setDeleteModalOpen(false);
|
||||||
|
}}
|
||||||
|
isModalOpen={deleteModalOpen}
|
||||||
|
onCloseModal={() => setDeleteModalOpen(false)}
|
||||||
|
triggerElement={<></>}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListViewRow
|
||||||
|
key={`h2-${member.firstName} ${member.lastName} ${member.birthday}`}
|
||||||
|
hoverable={true}
|
||||||
|
onClick={() => {}}
|
||||||
|
icon={<MemberPhoto base64String={member.image} />}
|
||||||
|
iconlabel={`${member.firstName} ${member.lastName}`}
|
||||||
|
invisibleActions={<DeleteMemberForm member={member} />}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
<Cell>{member.birthmonth}</Cell>
|
||||||
|
<Cell>{member.birthday}</Cell>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
dropdownActions={[
|
||||||
|
{
|
||||||
|
icon: <Edit style={DropDownIconCSS} />,
|
||||||
|
label: <>Rename</>,
|
||||||
|
action: () => {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Delete style={DropDownIconCSS} />,
|
||||||
|
label: <>Delete</>,
|
||||||
|
action: () => setDeleteModalOpen(true)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberRow;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Loading…
Reference in New Issue