diff --git a/app/package-lock.json b/app/package-lock.json index 2bde7e0..d9f5fb0 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1048,8 +1048,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "deep-extend": { "version": "0.5.1", @@ -4873,6 +4872,16 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "query-string": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.5.0.tgz", + "integrity": "sha512-TYC4hDjZSvVxLMEucDMySkuAS9UIzSbAiYGyA9GWCjLKB8fQpviFbjd20fD7uejCDxZS+ftSdBKE6DS+xucJFg==", + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "range-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", @@ -5540,6 +5549,11 @@ "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", "dev": true }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -5650,6 +5664,11 @@ "integrity": "sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg==", "dev": true }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", diff --git a/app/package.json b/app/package.json index 63d9f99..75caa06 100644 --- a/app/package.json +++ b/app/package.json @@ -13,6 +13,7 @@ "express-async-errors": "^3.1.1", "handlebars": "^4.0.11", "jsonwebtoken": "5.0.4", + "lodash": "^4.17.11", "method-override": "^2.3.5", "moment": "^2.10.3", "mongoose": "^4.13.18", @@ -20,6 +21,7 @@ "nodemailer": "^1.4.0", "nodemailer-smtp-transport": "^1.0.3", "passport-local": "^1.0.0", + "query-string": "^6.5.0", "request": "^2.60.0", "underscore": "^1.8.3", "validator": "^3.40.1" diff --git a/app/server/controllers/UserController.js b/app/server/controllers/UserController.js index 791afed..d22326b 100644 --- a/app/server/controllers/UserController.js +++ b/app/server/controllers/UserController.js @@ -651,7 +651,17 @@ UserController.admitUser = function(id, user, callback){ }, { new: true }, - callback); + function (err, user) { + if (err) { + return callback(err); + } + + if (!user) { + return callback({ message: 'user cannot be admitted' }) + } + + return callback(null, user); + }); }); }; diff --git a/app/server/routes/api/users.js b/app/server/routes/api/users.js index db5e08e..d8a2540 100644 --- a/app/server/routes/api/users.js +++ b/app/server/routes/api/users.js @@ -125,7 +125,7 @@ router.post('/:id/unfavoriteFirebaseEvent/:firebaseId', isOwnerOrAdmin, async fu }); /** - * Admit a user. ADMIN ONLY, DUH + * Admit a user. Admin only. * * Also attaches the user who did the admitting, for liabaility. */ @@ -137,7 +137,7 @@ router.post('/:id/admit', isAdmin, function (req, res) { }); /** - * Check in a user. Admin or Organizer + * Check in a user. Admin or Organizer. */ router.post('/:id/checkin', isOrganizerOrAdmin, function (req, res) { const id = req.params.id; @@ -147,7 +147,7 @@ router.post('/:id/checkin', isOrganizerOrAdmin, function (req, res) { }); /** - * Check in a user. Admin or Organizer + * Check out a user. Admin or Organizer. */ router.post('/:id/checkout', isOrganizerOrAdmin, function (req, res) { const id = req.params.id; diff --git a/client/src/components/Admin/AdminNav.js b/client/src/components/Admin/AdminNav.js new file mode 100644 index 0000000..68a7c72 --- /dev/null +++ b/client/src/components/Admin/AdminNav.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Menu } from 'semantic-ui-react'; + +@observer +class AdminNav extends React.Component { + render() { + const { activePage } = this.props; + + return ( + + this.props.pushHistory('/admin/stats')} /> + this.props.pushHistory('/admin/users')} /> + this.props.pushHistory('/admin/settings')} /> + this.props.pushHistory('/admin/events')} /> + this.props.pushHistory('/admin/actions')} /> + + ) + } +} + +export default AdminNav; \ No newline at end of file diff --git a/client/src/components/Admin/AdminStatsDisplay.js b/client/src/components/Admin/AdminStatsDisplay.js new file mode 100644 index 0000000..5778bcd --- /dev/null +++ b/client/src/components/Admin/AdminStatsDisplay.js @@ -0,0 +1,16 @@ +import * as React from 'react'; +import * as _ from 'lodash'; + +class AdminStatsDisplay extends React.Component { + render() { + const { stats } = this.props; + + return ( +
+        {JSON.stringify(stats, null, 2)}
+      
+ ) + } +} + +export default AdminStatsDisplay; \ No newline at end of file diff --git a/client/src/components/Admin/AdminUsers/UserModal.js b/client/src/components/Admin/AdminUsers/UserModal.js new file mode 100644 index 0000000..ff257d7 --- /dev/null +++ b/client/src/components/Admin/AdminUsers/UserModal.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Modal } from 'semantic-ui-react'; + +const UserModal = ({ user, ...rest}) => { + return ( + + {JSON.stringify(user, null, '\t')} + } + /> + ); +} + +export default observer(UserModal); \ No newline at end of file diff --git a/client/src/components/Admin/AdminUsers/UsersLayout.js b/client/src/components/Admin/AdminUsers/UsersLayout.js new file mode 100644 index 0000000..cbcc0df --- /dev/null +++ b/client/src/components/Admin/AdminUsers/UsersLayout.js @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { Grid, Header } from 'semantic-ui-react'; +import LoadingDisplay from '../../LoadingDisplay'; + +@observer +class UsersLayout extends React.Component { + render() { + const { + usersSearch, + usersTable, + loading, + userModal, + admitModal, + checkinModal, + } = this.props; + + return ( + + + +
+ {usersSearch} + + +
+ {loading + ? + : usersTable} + + + {userModal} + {admitModal} + {checkinModal} + + ) + } +} + +export default UsersLayout; \ No newline at end of file diff --git a/client/src/components/Admin/AdminUsers/UsersSearch.js b/client/src/components/Admin/AdminUsers/UsersSearch.js new file mode 100644 index 0000000..103b3ca --- /dev/null +++ b/client/src/components/Admin/AdminUsers/UsersSearch.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import * as _ from 'lodash'; +import { Button, Input, Label } from 'semantic-ui-react'; + +@observer +class UsersSearch extends React.Component { + constructor(props) { + super(props); + + this.state = { intermediatePageSize: props.pageSize }; + } + + componentDidUpdate(prevProps) { + if (prevProps.pageSize !== this.props.pageSize) { + this.setState({ intermediatePageSize: this.props.pageSize }); + } + } + + render() { + return ( +
+
+
+ + + + +
+ ); + } + + handleSearchChange = (e, { value }) => { + this.props.onChangeQuery(value); + } + + handlePageSizeChange = (e, { value }) => { + this.setState({ intermediatePageSize: value }); + } + + handlePageSizeSubmit = () => { + this.props.onChangePageSize(Number(this.state.intermediatePageSize)); + } +} + +export default UsersSearch; \ No newline at end of file diff --git a/client/src/components/Admin/AdminUsers/UsersTable.js b/client/src/components/Admin/AdminUsers/UsersTable.js new file mode 100644 index 0000000..099eefe --- /dev/null +++ b/client/src/components/Admin/AdminUsers/UsersTable.js @@ -0,0 +1,112 @@ +import React from 'react'; +import { observer } from 'mobx-react'; +import { Button, Icon, Table } from 'semantic-ui-react'; + +const checkIcon = +const xIcon = + +@observer +class UserRow extends React.Component { + + render () { + const { user } = this.props; + const rowStatus = {}; + switch (user.status.name) { + case 'confirmed': + rowStatus.positive = true; + break; + case 'admitted': + rowStatus.warning = true; + break; + case 'declined': + rowStatus.error = true; + break; + default: + break; + }; + + const name = user.status.completedProfile + ? `${user.profile.firstName} ${user.profile.lastName}` + : user.email; + const email = user.email; + const school = user.status.completedProfile ? user.profile.school.trim() : ""; + const year = user.status.completedProfile ? user.profile.schoolYear.trim() : ""; + + const verified = user.verified; + const submitted = user.status.completedProfile; + const admitted = user.status.admitted; + const confirmed = user.status.confirmed; + + const admitButton = ( +