Backup checkpoint one

This commit is contained in:
Steven Fan 2024-05-09 23:36:25 -04:00
parent 164a60e336
commit a1ad767261
19 changed files with 783 additions and 156 deletions

View File

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

View File

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

View File

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

2
client/src/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare module '*.jpg';
declare module '*.png';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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