Skip to content

Commit

Permalink
Member profile page (#23)
Browse files Browse the repository at this point in the history
* Create basic endpoint skeleton

* get endpoint 500 error

* Fix routing

* Get member endpoint to work

* Return more fields from endpoint

* Return object id from endpoint

* Add filterSensitiveInfo util

* Give all non-required Member fields a default of null

* Move passport-setup to utils

* Remove console.log

* Add members endpoints

* Fix filterSensitiveInfo util

* Use passport-setup.js in utils

* Use auth endpoint

* Move /api/auth/user to /api/members/current

* Add omit fields to GET /:memberId

* Change current user endpoint in frontend

* Add more auth utils

* Use isDirector from module in members routes

* Change level default in model back to TBD

* Add level priority explanation

* Add detailed permission checking in members endpoint

* Fix difference function

* Fix typos in user-utils

* Add _id as neverEditable field

* Fix allFields not being checked correctly

* Filter viewable fields when returning from PUT

* Pull enum options from backend

* Add status options to endpoint

* Disable dropdowns

* Add labels to fields

* Create wrapper for getting member by ID

* Add boolean selector

* Add number fields

* Add fields for basic string attributes

* Retrieve schema type from DB

* Move preprocessing to backend

* Fix enum type detection

* Remove duplicated code

* Fix attribute change error

* Run formatter

* Remove warnings

* Format client

* Populate edit fields from DB

* Add types to props

* Run formatter

* Remove alert

* Run formatter

* Get permissions from backend

* Disable input boxes if missing permissions

* Add enum dropdowns

* Format client

* Remove user ID override

* Change to ES6 defaults

* Remove unused package

* Change var to let

* Concisen member options endpoint

* Delete env file

* Unexport schema

* Use concise property iteration

* Make /options endpoint concise

* Remove unneeded exports

* Capitalize constant

* Shorten attribute change callback

* Run formatter

* Turn off default-props in linter

* Fix var name typo

* Change member page routing

* Run formatter

* Use useParams hook

* Change object syntax

* Run formatter

* Link "View Profile" to current logged-in user

* Fix lint, ignore _id

* Run format

* Don't show mongo ID

* Rename Member.js to Profile.js

* Run formatter

* Store fields in value instead of placeholder

Co-authored-by: ishaansharma <[email protected]>
Co-authored-by: Jeffrey Tang <[email protected]>
  • Loading branch information
3 people authored Jan 16, 2021
1 parent c74feb5 commit 27ed934
Show file tree
Hide file tree
Showing 17 changed files with 1,397 additions and 48 deletions.
5 changes: 0 additions & 5 deletions api/config/dev.env

This file was deleted.

49 changes: 49 additions & 0 deletions api/src/api/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,55 @@ router.post(
}),
);

// Gives the enum options to the frontend to populate the dropdown.
// The key in the 'options' object must be the same as the DB attribute.
router.get(
'/options',
requireRegistered,
errorWrap(async (req, res) => {
const options = {};
Member.schema.eachPath((pathname, schemaType) => {
const { enum: optionsEnum } = schemaType.options;
if (optionsEnum) {
options[pathname] = optionsEnum.map((option) => ({
label: option,
value: option,
}));
}
});

res.json({
success: true,
result: options,
});
}),
);

// Returns the types of all the schema properties for member. This allows the frontend
// to decide the best way to provide input for each attribute.
// Note: This will only work for non-nested fields
router.get(
'/schema',
requireRegistered,
errorWrap(async (req, res) => {
const schemaTypes = { Enum: [] };

Member.schema.eachPath((pathname, schemaType) => {
if (schemaTypes[schemaType.instance] == null)
schemaTypes[schemaType.instance] = [];

if (schemaType.enumValues != null && schemaType.enumValues.length > 0)
schemaTypes['Enum'].push(pathname);
else schemaTypes[schemaType.instance].push(pathname);
});

res.json({
success: true,
result: schemaTypes,
});
}),
);

router.get(
'/:memberId',
requireRegistered,
Expand Down
2 changes: 2 additions & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
],
"plugins": ["flowtype", "import", "react", "react-hooks"],
"rules": {
"react/require-default-props": "off",
"no-underscore-dangle": ["error", { "allow": ["_id"] }],
"import/order": [
"warn",
{
Expand Down
5 changes: 4 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
"homepage": "https://pineapple.lol/",
"license": "MIT",
"dependencies": {
"@hack4impact-uiuc/bridge": "^1.1.0",
"ag-grid-community": "^25.0.0",
"ag-grid-enterprise": "^25.0.0",
"ag-grid-react": "^25.0.0",
"axios": "^0.21.1",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-datepicker": "^3.4.1",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3"
"react-scripts": "3.4.3",
"react-select": "^3.2.0"
},
"devDependencies": {
"@babel/cli": "^7.6.2",
Expand Down
7 changes: 4 additions & 3 deletions client/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Route, Switch, Redirect, useLocation } from 'react-router-dom';

import * as Routes from './routes';
import Home from './pages/Home';
import Member from './pages/Member';
import Profile from './pages/Profile';
import Login from './pages/Login';
import Navbar from './components/navbar/Navbar';
import { getUserAuth } from './utils/apiWrapper';
Expand Down Expand Up @@ -31,8 +31,9 @@ function App() {
<Route path={Routes.DEFAULT} exact>
{user ? <Home user={user} /> : <Redirect to={Routes.LOGIN_PAGE} />}
</Route>
<Route path={Routes.MEMBER_PAGE} exact>
{user ? <Member user={user} /> : <Redirect to={Routes.LOGIN_PAGE} />}
<Route path={Routes.MEMBER_PAGE}>
{/* TODO: Replace this with the users ID once we get real data in the DB */}
{user ? <Profile /> : <Redirect to={Routes.LOGIN_PAGE} />}
</Route>
</Switch>
</div>
Expand Down
35 changes: 35 additions & 0 deletions client/src/components/EditableAttribute/BooleanAttribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';

import EnumAttribute from './EnumAttribute';

const BooleanAttribute = ({
value = '',
attributeLabel = '',
isDisabled = false,
onChange,
}) => {
const VALUE_OPTIONS = [
{ label: 'Yes', value: 'true' },
{ label: 'No', value: 'false' },
];

return (
<EnumAttribute
value={value}
attributeLabel={attributeLabel}
valueOptions={VALUE_OPTIONS}
isDisabled={isDisabled}
onChange={onChange}
/>
);
};

BooleanAttribute.propTypes = {
value: PropTypes.string,
attributeLabel: PropTypes.string,
isDisabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};

export default BooleanAttribute;
35 changes: 35 additions & 0 deletions client/src/components/EditableAttribute/DateAttribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';

const DateAttribute = ({
value = '',
attributeLabel = '',
isDisabled = false,
onChange,
}) => {
const onValueChange = date => {
onChange(date, attributeLabel);
};

return (
<div>
<p>{attributeLabel}</p>
<DatePicker
onChange={onValueChange}
selected={value}
disabled={isDisabled}
/>
</div>
);
};

DateAttribute.propTypes = {
value: PropTypes.string,
attributeLabel: PropTypes.string,
isDisabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};

export default DateAttribute;
40 changes: 40 additions & 0 deletions client/src/components/EditableAttribute/EnumAttribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';

const EnumAttribute = ({
value = '',
valueOptions = [],
attributeLabel = '',
isDisabled = false,
onChange,
}) => {
const onValueChange = option => {
onChange(option, attributeLabel);
};

return (
<div>
<p>{attributeLabel}</p>
<Select
defaultValue={value}
value={value}
placeholder={value}
isDisabled={isDisabled}
name={attributeLabel}
options={valueOptions}
onChange={onValueChange}
/>
</div>
);
};

EnumAttribute.propTypes = {
value: PropTypes.string,
valueOptions: PropTypes.arrayOf(PropTypes.string),
attributeLabel: PropTypes.string,
isDisabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};

export default EnumAttribute;
37 changes: 37 additions & 0 deletions client/src/components/EditableAttribute/StringAttribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TextField } from '@hack4impact-uiuc/bridge';

const StringAttribute = ({
type = 'text',
value = '',
attributeLabel = '',
isDisabled = false,
onChange,
}) => {
const onValueChange = e => {
onChange(e.target.value, attributeLabel);
};

return (
<div>
<p>{attributeLabel}</p>
<TextField
type={type}
value={value}
onChange={onValueChange}
disabled={isDisabled}
/>
</div>
);
};

StringAttribute.propTypes = {
type: PropTypes.string,
value: PropTypes.string,
attributeLabel: PropTypes.string,
isDisabled: PropTypes.bool,
onChange: PropTypes.func.isRequired,
};

export default StringAttribute;
14 changes: 12 additions & 2 deletions client/src/components/Profile/Profile.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import '../../css/Profile.css';
import { Redirect } from 'react-router-dom';

Expand All @@ -8,8 +9,9 @@ import { endUserSession } from '../../utils/apiWrapper';

/**
* Displays the Profile icon in the navbar + dropdown components for logout and viewing profile
* @param {Object} user the current user of the session
*/
const Profile = () => {
const Profile = ({ user }) => {
const [redirectToMemberPage, setRedirectToMemberPage] = useState(false);
const [isLoggedOut, setIsLoggedOut] = useState(false);

Expand All @@ -33,7 +35,9 @@ const Profile = () => {
return (
<div className="dropdown">
{/** JSX Redirects */}
{redirectToMemberPage && <Redirect to={Routes.MEMBER_PAGE} />}
{redirectToMemberPage && (
<Redirect to={Routes.MEMBER_PAGE.replace(':memberID', user._id)} />
)}
{isLoggedOut && <Redirect to={Routes.LOGIN_PAGE} />}

{/** Rendered JSX */}
Expand All @@ -54,4 +58,10 @@ const Profile = () => {
);
};

Profile.propTypes = {
user: PropTypes.shape({
_id: PropTypes.string.isRequired,
}).isRequired,
};

export default Profile;
10 changes: 4 additions & 6 deletions client/src/components/navbar/Navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const Navbar = ({ user }) => (
Hello,
{user.firstName}!
</h2>
<Profile />
<Profile user={user} />
</div>
</nav>
);
Expand All @@ -32,11 +32,9 @@ Navbar.propTypes = {
user: PropTypes.shape({
_id: PropTypes.string.isRequired,
firstName: PropTypes.string.isRequired,
lastName: PropTypes.string.isRequired,
oauthID: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
level: PropTypes.string.isRequired,
__v: PropTypes.number.isRequired,
lastName: PropTypes.string,
email: PropTypes.string,
level: PropTypes.string,
}).isRequired,
};

Expand Down
1 change: 1 addition & 0 deletions client/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import '@hack4impact-uiuc/bridge/dist/styles.css';

import App from './App';

Expand Down
5 changes: 0 additions & 5 deletions client/src/pages/Member.js

This file was deleted.

Loading

1 comment on commit 27ed934

@vercel
Copy link

@vercel vercel bot commented on 27ed934 Jan 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.