Compare commits

...

2 Commits

Author SHA1 Message Date
Steven Fan a1ad767261 Backup checkpoint one 2024-05-09 23:36:25 -04:00
Steven Fan 164a60e336 Use dev db 2024-05-09 07:37:18 -04:00
21 changed files with 785 additions and 158 deletions

View File

@ -51,6 +51,7 @@
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-jss": "^10.10.0",
"react-phone-input-2": "^2.15.1",
"react-redux": "^8.1.2",
"react-router-dom": "^6.11.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",
"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": {
"version": "5.3.2",
"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",
"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": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"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": {
"version": "4.5.0",
"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",
"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": {
"version": "8.1.2",
"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-dom": "^18.2.0",
"react-jss": "^10.10.0",
"react-phone-input-2": "^2.15.1",
"react-redux": "^8.1.2",
"react-router-dom": "^6.11.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 { useState } from "react";
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";
import { selectMemberLoading, selectMemberUpdating } from "./store/selectors/member.selectors";
import Directory from "./views/Directory/Directory";
export const DIRECTORY = "/";
RegisterChart.register(...registerables);
function App() {
const [initialLoad, setInitialLoad] = useState<boolean>(true);
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>
{/* <ReactKeycloakProvider authClient={keycloak}> */}
{/* <AuthGuard> */}
<BrowserRouter>
<Routes>
<Route path={DIRECTORY} Component={Directory} />
</Routes>
</BrowserRouter>
{/* </AuthGuard> */}
{/* </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({
url: KeycloakURL,
realm: "directory",
clientId: "React-auth",
});
// const keycloak = new Keycloak({
// url: KeycloakURL,
// realm: "directory",
// clientId: "React-auth",
// });
export default keycloak;
// export default keycloak;

View File

@ -2,7 +2,7 @@ import { CRUDService } from "../shared/couchconnector/CouchCrudService";
import { Member } from "../types/member";
import CouchCrudService from "../shared/couchconnector/CouchCrudService";
const url = "member_photos";
const url = "dev_member_photos";
const memberCouchDataService = new CouchCrudService<Member>("MemberCouchDataService", url);
class MemberCouchService implements CRUDService<Member> {

View File

@ -12,6 +12,7 @@ interface CreateFormProps<Document> {
isDocument: (object: any) => object is Document;
formInputs: formInput[];
triggerElement: JSX.Element;
title?: string;
}
type CreateFormState = {};
@ -29,13 +30,13 @@ export default class CreateForm<Document> extends React.Component<CreateFormProp
render() {
// 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 (
<Modal<Document>
onSubmitForm={this.onFormSubmit}
triggerElement={triggerElement}
modalTitle={""}
modalTitle={title || ""}
modalDescription=""
submitFormButtonText="Save"
formInputs={formInputs}

View File

@ -2,6 +2,7 @@ import { Dictionary } from "lodash";
import React from "react";
import { SPACE_BETWEEN_HORIZONTALLY } from "../../../utils/styles";
import Select from "../Select/Select";
import PhoneInput from "react-phone-input-2";
export type formInput = {
name: string;
@ -9,6 +10,7 @@ export type formInput = {
validationMessage: string;
inputProps: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
options?: Dictionary<string>;
style?: React.CSSProperties;
};
export type FormProps = {
@ -84,7 +86,7 @@ export default class Form extends React.Component<FormProps, FormState> {
<form className="FormRoot" onSubmit={this.onSubmit}>
{formInputs.map((formInput) => {
return (
<div key={formInput.name} className="FormField">
<div key={formInput.name} className="FormField" style={formInput.style}>
{/* Range Input additional label */}
{formInput.inputProps.type && formInput.inputProps.type === "range" ? (
<label className="FormLabel">
@ -110,7 +112,16 @@ export default class Form extends React.Component<FormProps, FormState> {
const fields = this.state.fields;
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

View File

@ -12,12 +12,16 @@ input {
}
.FormRoot {
width: 260px;
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 2em;
}
.FormField {
display: grid;
margin-bottom: 10px;
flex: 1;
display: flex;
flex-direction: column;
}
.FormLabel {
@ -50,11 +54,13 @@ input {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 450px;
max-height: 100vh;
min-width: 30vw;
max-width: 90vw;
max-height: 80vh;
padding: 25px;
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
overflow-y: auto;
}
.DialogContent:focus {
@ -165,7 +171,7 @@ input {
.Label {
font-size: 15px;
color: var(--violet11);
/* color: var(--violet11); */
width: 90px;
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 "./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.
@ -42,16 +41,16 @@ export default class Select extends React.Component<SelectProps, SelectState> {
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.Trigger className={"Input"} 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.Content>
<RadixSelect.Viewport className="SelectViewport">
<RadixSelect.Group>
<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.Portal>
</RadixSelect.Root>
</GraphOption.Select>
</>
);
}
}

View File

@ -21,12 +21,15 @@ button {
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);
}
@ -37,7 +40,6 @@ button {
.SelectContent {
overflow: hidden;
background-color: white;
border-radius: 6px;
box-shadow:
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
@ -46,6 +48,10 @@ button {
.SelectViewport {
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 {
@ -60,10 +66,12 @@ button {
position: relative;
user-select: none;
}
.SelectItem[data-disabled] {
color: var(--mauve8);
pointer-events: none;
}
.SelectItem[data-highlighted] {
outline: none;
background-color: var(--violet9);
@ -100,4 +108,4 @@ button {
background-color: white;
color: var(--violet11);
cursor: default;
}
}

View File

@ -1,7 +1,7 @@
import { Dictionary } from "lodash";
const envToRemoteMapping: Dictionary<string> = {
LOCAL: "",
LOCAL: "http://localhost:21288/",
DEV: "",
STAGE: "",
PROD: ""

View File

@ -43,8 +43,6 @@ const membersSlice = createSlice({
});
})
.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)

View File

@ -4,6 +4,7 @@ import { couchDocument } from "./CouchDocument";
export type Member = couchDocument & {
apt: string;
birthday: string;
birthmonth?: string;
city: string;
communityGroup: string;
email: string;
@ -30,6 +31,7 @@ export const MemberUtils = {
_id: uuidGenerator(),
apt: "",
birthday: "",
birthmonth: "",
city: "",
communityGroup: "",
email: "",
@ -48,19 +50,244 @@ export const MemberUtils = {
streetType: "",
userLogin: "",
zipCode: "",
// image: ""
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",
apt: {
name: "apt",
label: "Apt #",
validationMessage: "Leave blank space if none.",
inputProps: {
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 { 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";
// 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;
}
// interface PrivateRouteProps {
// children?: ReactNode;
// }
const LoginContainer = styled.div`
${CenterHorizontally}
width: 100%;
height: 100vh;
// const LoginContainer = styled.div`
// ${CenterHorizontally}
// width: 100%;
// height: 100vh;
padding-top: 10%;
// padding-top: 10%;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
`;
// 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 LoginMenu = styled.div`
// display: flex;
// flex-direction: column;
// gap: 0.5em;
// `;
const LoginMessage = styled.div`
${CenterHorizontally}
${CenterVertically}
// const LoginMessage = styled.div`
// ${CenterHorizontally}
// ${CenterVertically}
padding-bottom: 1em;
width: 100%;
font-size: 2em;
font-weight: 500;
`;
// padding-bottom: 1em;
// width: 100%;
// font-size: 2em;
// font-weight: 500;
// `;
const NewUserButton = styled(Button)`
width: 80%;
margin: 0 auto;
// const NewUserButton = styled(Button)`
// width: 80%;
// margin: 0 auto;
padding: 1em;
// padding: 1em;
/* border: 1px solid #b8e5fa; */
border-radius: 60px;
// /* border: 1px solid #b8e5fa; */
// border-radius: 60px;
color: #0080ff;
background-color: #def8ff;
// color: #0080ff;
// background-color: #def8ff;
font-size: 1.1em;
`;
// font-size: 1.1em;
// `;
const ExisitingUserButton = styled(Button)`
width: 80%;
margin: 0 auto;
// const ExisitingUserButton = styled(Button)`
// width: 80%;
// 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 { keycloak } = useKeycloak();
const dispatch = useDispatch();
// const AuthGuard: FC<PrivateRouteProps> = ({ children }: PrivateRouteProps) => {
// const { keycloak } = useKeycloak();
// 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(() => {
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]);
// 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);
};
// const login = () => {
// if (isLoggedIn) return;
// keycloak.login();
// setLogging(true);
// };
keycloak.onAuthSuccess = function () {
setLogging(false);
};
// keycloak.onAuthSuccess = function () {
// setLogging(false);
// };
return isLoggedIn ? (
children
) : (
<>
<Header title={<PageHeader.Container>directory</PageHeader.Container>} noActions={true} />
// 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>
)}
</>
);
};
// {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;
// export default AuthGuard;

View File

@ -2,30 +2,171 @@ 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 { Member, MemberUtils } from "../../types/member";
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 [initialLoad, setInitialLoad] = useState<boolean>(true);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const dispatch = useDispatch<AppDispatch>();
const members: Dictionary<Member> = useSelector(selectMembers);
useEffect(() => {
initialLoad && dispatch(memberActions.fetchMembers());
setInitialLoad(false);
}, []);
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 (
<>
<h1>Directory</h1>
<Title>
<LogoContainer src={Logo} />
NCBC Members Directory ({currentMonth} {currentYear} Edition)
</Title>
{/* <Button onClick={customAction}>Custom Action</Button> */}
{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>
))
<Table>
{Object.values(members)
.sort((a, b) => (a.lastName + a.firstName).localeCompare(b.lastName + b.firstName))
.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

View File

@ -2,7 +2,7 @@ import { ServerScope } from "nano";
import { Member } from "../types/member";
import { CrudService } from "./CrudService";
export const MEMBER_DB_ENDPOINT = "member_photos";
export const MEMBER_DB_ENDPOINT = "dev_member_photos";
export const MemberService = (databaseServer: ServerScope) => {
const memberDatabase = databaseServer.use<Member>(MEMBER_DB_ENDPOINT);