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 (
+
+ )
+ }
+}
+
+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 = (
+