├── .env.example ├── .gitignore ├── .idea ├── .gitignore ├── helpqueue.iml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── HOWTHISWASMADE.md ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── client ├── .gitignore ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── AppHeader.tsx │ ├── Types.tsx │ ├── components │ │ ├── AdminPage.tsx │ │ ├── Alert.tsx │ │ ├── FAQPage.tsx │ │ ├── LandingPage.tsx │ │ ├── LoginCallback.tsx │ │ ├── LoginGithub.tsx │ │ ├── ProfilePage.tsx │ │ ├── QueueMentor.tsx │ │ ├── QueueRequest.tsx │ │ ├── ServerHelper.tsx │ │ ├── Types.tsx │ │ └── info.md │ ├── hooks │ │ ├── info.md │ │ ├── useLogin.tsx │ │ └── useViewer.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ └── serviceWorker.ts ├── tsconfig.json └── yarn.lock ├── docs └── img │ ├── admin.png │ ├── mentor.png │ └── opening.png ├── manage.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 653093fddc44_.py │ ├── 68d234e0f83e_.py │ ├── 8a8e177c22c8_.py │ ├── 8d950e758485_.py │ ├── a9fd5b5f5b0a_.py │ ├── de3ab01f4eb0_.py │ └── eef69a932db1_.py ├── package.json ├── prebuild.py ├── requirements.txt ├── run_dev_server.py ├── run_server.py ├── server ├── __init__.py ├── api │ └── v1 │ │ ├── __init__.py │ │ ├── api.py │ │ ├── api_admin.py │ │ ├── api_login.py │ │ ├── api_tickets.py │ │ ├── api_user.py │ │ └── info.md ├── app.py ├── cache.py ├── controllers │ ├── authentication.py │ ├── cron.py │ ├── dopeauth.py │ ├── info.md │ ├── settings.py │ ├── tickets.py │ └── users.py ├── helpers.py ├── models │ ├── __init__.py │ ├── client.py │ ├── setting.py │ ├── ticket.py │ └── user.py └── server_constants.py ├── update_and_deploy.sh └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | DEVELOPMENT=True 3 | SECRET_KEY='SECRET' 4 | SQLALCHEMY_DATABASE_URI="postgresql://localhost/helpq" 5 | SQLALCHEMY_TRACK_MODIFICATIONS="False" 6 | FLASK_ENV="developement" 7 | PORT=3000 8 | 9 | MASTER_EMAIL="kevin21@mit.edu" 10 | REACT_APP_SITEURL="http://localhost:3000" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | .env 3 | **/__pycache__/ 4 | service_account.json 5 | env.sh 6 | 7 | 8 | /node_modules 9 | /server/build -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/helpqueue.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/Users/kevin/.pyenv/shims/python" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to HelpLIFO 2 | ===================== 3 | 4 | Thank you for using HelpLIFO, and we're really excited that you want to help out! 5 | 6 | Right now the code base is still not docstringed out yet so fun! 7 | 8 | Pull Requests 9 | ------------- 10 | 11 | We actively welcome your pull requests! 12 | 13 | However, before you begin, please create an issue so we can determine if the work, feature, or bugfix either has someone already working on it as a part of the current roadmap, or if the feature is not something that would belong in the HELPq as a core feature. 14 | 15 | This is mostly meant for discussion, so we can discuss things fully and make a really awesome product :) 16 | 17 | Issues 18 | ------ 19 | 20 | We use issues to track public bugs. Please make sure your description is clear and has sufficient instructions to be able to reproduce the issue. 21 | 22 | We also use issues to track feature requests and discuss. Please mark your issue as 'Feature Request' in this case! 23 | 24 | 25 | Coding Style 26 | ------------ 27 | 28 | Keep it clean, and keep things modular! -------------------------------------------------------------------------------- /HOWTHISWASMADE.md: -------------------------------------------------------------------------------- 1 | `create-react-app client --typescript` 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin Fang (TechX) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run gunicorn -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Help Queue 2 | 3 | The easy to use help queue system, built on top of Flask + React! 4 | 5 | Created and maintained by: [TheReddKing](mailto:kevin21@mit.edu) ([TechX](https://techx.io)) 6 | 7 | 8 | ### Heroku Deploy 9 | 10 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/techx/helpqueue/tree/master) 11 | 12 | There are two variables you need to set the master email account and also the URL of the site. These must be edited as heroku env variables. 13 | All other settings can be edited on the `/admin` page 14 | 15 | ### Screenshots 16 | 17 | ![Home Screen](./docs/img/opening.png) 18 | ![Mentor Screen](./docs/img/mentor.png) 19 | ![Admin Screen](./docs/img/admin.png) 20 | 21 | ## Dev: 22 | ### Local Installation: 23 | 24 | python -m venv env 25 | source env/bin/activate 26 | pip install --upgrade pip 27 | pip install -r requirements.txt 28 | yarn 29 | cp .env.example .env 30 | cd client && yarn 31 | 32 | Then edit your `.env` file. Once your database url is correct (you can use `createdb helpq` if you have postgres) 33 | 34 | python manage.py db upgrade 35 | 36 | [Windows might mess up things click here to fix](https://stackoverflow.com/questions/18664074/getting-error-peer-authentication-failed-for-user-postgres-when-trying-to-ge) 37 | 38 | ### Dev run 39 | 40 | yarn run dev 41 | 42 | or (if you want to debug server side scripts or if u are on windows) 43 | 44 | yarn start 45 | yarn run dev-server 46 | 47 | ### Modifying for events 48 | 49 | See [FILESTRUCTURE.md](FILESTRUCTURE.md). 50 | 51 | Contributing 52 | ------------ 53 | 54 | I'd love to take pull requests! Please read the [Contributing Guide](CONTRIBUTING.md) first! 55 | 56 | Other TODOs: 57 | - App Success Deploy 58 | { 59 | "success_url": "/welcome" 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TechX Help Queue", 3 | "description": "A simple help queue interface for anything!", 4 | "repository": "https://github.com/techx/helpqueue", 5 | "keywords": ["helpq", "help queue"], 6 | "addons": [ 7 | { 8 | "plan": "heroku-postgresql", 9 | "options": { 10 | "version": "12" 11 | } 12 | } 13 | ], 14 | "buildpacks": [ 15 | { 16 | "url": "heroku/python" 17 | }, 18 | { 19 | "url": "heroku/nodejs" 20 | } 21 | ], 22 | "env": { 23 | "REACT_APP_SITEURL": { 24 | "description": "URL of site (usually https://[appname].herokuapp.com) YOU NEED https or http", 25 | "value": "https://[appname].herokuapp.com" 26 | }, 27 | "MASTER_EMAIL": { 28 | "description": "Email of the default ADMIN user (prevents their admin rights from being revoked)", 29 | "value": "kevin21@mit.edu" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "helplifo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-svg-core": "^1.2.26", 7 | "@fortawesome/free-solid-svg-icons": "^5.12.0", 8 | "@fortawesome/react-fontawesome": "^0.1.8", 9 | "@types/jest": "24.0.16", 10 | "@types/node": "12.6.9", 11 | "@types/react": "16.8.24", 12 | "@types/react-dom": "16.8.5", 13 | "@types/react-router": "^5.0.3", 14 | "@types/react-router-dom": "^4.3.4", 15 | "@types/react-s-alert": "^1.3.2", 16 | "@types/react-table": "^6.8.5", 17 | "@types/react-tagsinput": "^3.19.6", 18 | "@types/reactstrap": "^8.0.1", 19 | "@types/styled-components": "^4.4.1", 20 | "bootstrap": "^4.3.1", 21 | "concurrently": "^4.1.1", 22 | "dotenv": "^8.0.0", 23 | "jquery": "^3.4.1", 24 | "jsoneditor": "^8.0.0", 25 | "jsoneditor-react": "^2.0.0", 26 | "popper.js": "^1.15.0", 27 | "prop-types": "^15.7.2", 28 | "rc-rate": "^2.5.0", 29 | "react": "^16.8.6", 30 | "react-cookie": "^4.0.1", 31 | "react-dom": "^16.8.6", 32 | "react-router": "^5.0.1", 33 | "react-router-dom": "^5.0.1", 34 | "react-s-alert": "^1.4.1", 35 | "react-scripts": "^3.0.1", 36 | "react-table": "^7.0.0-rc.10", 37 | "react-table-6": "^6.11.0", 38 | "react-tagsinput": "^3.19.0", 39 | "reactstrap": "^8.0.1", 40 | "semantic-ui-react": "^0.88.2", 41 | "styled-components": "^4.4.1", 42 | "typescript": "3.5.3" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "scripts": { 48 | "start": "react-scripts start", 49 | "build": "react-scripts build", 50 | "test": "react-scripts test", 51 | "eject": "react-scripts eject" 52 | }, 53 | "proxy": "http://localhost:5000", 54 | "browserslist": { 55 | "production": [ 56 | ">0.2%", 57 | "not dead", 58 | "not op_mini all" 59 | ], 60 | "development": [ 61 | "last 1 chrome version", 62 | "last 1 firefox version", 63 | "last 1 safari version" 64 | ] 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techx/helpqueue/f524f1bc6ad9675d5a8f80a24bca89b27814beb1/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | 6 | .App-logo { 7 | animation: App-logo-spin infinite 20s linear; 8 | height: 40vmin; 9 | pointer-events: none; 10 | } 11 | 12 | .App-header { 13 | background-color: #1B4481; 14 | min-height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | font-size: calc(10px + 2vmin); 20 | color: white; 21 | } 22 | 23 | .App-link { 24 | color: #61dafb; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | 36 | .App .ui.card { 37 | width: 100%; 38 | margin: auto; 39 | box-shadow: 0px 8px 20px -10px rgba(13, 28, 39, 0.6); 40 | background: #fff; 41 | border-radius: 15px; 42 | max-width: 700px; 43 | position: relative; 44 | margin-top: 2px; 45 | margin-bottom: 2px; 46 | padding: 1em; 47 | } 48 | 49 | .App .ui.card h2:after { 50 | margin-top: .1em; 51 | margin-bottom: .1em; 52 | content:' '; 53 | display:block; 54 | border:2px solid #001D55; 55 | border-radius:4px; 56 | -webkit-border-radius:4px; 57 | -moz-border-radius:4px; 58 | box-shadow:inset 0 1px 1px rgba(0, 0, 0, .05); 59 | -webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .05); 60 | -moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, .05); 61 | } 62 | 63 | .App .ui.card .ui.card { 64 | box-shadow: 0px 0px 0px 0px white; 65 | border: 1px solid gray; 66 | } 67 | 68 | .App .card .card .card-title:after { 69 | border:0px solid#3AA5B0; 70 | } 71 | .App .noshadow .card { 72 | box-shadow: 0; 73 | } 74 | 75 | .ui.label.label{ 76 | background-color: #FFDF3F 77 | } 78 | 79 | /* FAQ PAGE STYLING */ 80 | 81 | .App .accordion { 82 | text-align:left; 83 | } 84 | -------------------------------------------------------------------------------- /client/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import "./App.css"; 3 | import { CookiesProvider } from "react-cookie"; 4 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 5 | 6 | import AppHeader from "./AppHeader"; 7 | import LandingPage from "./components/LandingPage"; 8 | import LoginCallback from "./components/LoginCallback"; 9 | import LoginGithub from "./components/LoginGithub"; 10 | import QueueRequest from "./components/QueueRequest"; 11 | import QueueMentor from "./components/QueueMentor"; 12 | import AdminPage from "./components/AdminPage"; 13 | import ProfilePage from "./components/ProfilePage"; 14 | import FAQPage from "./components/FAQPage"; 15 | 16 | import Alert from "react-s-alert"; 17 | 18 | import "react-s-alert/dist/s-alert-default.css"; 19 | import "react-s-alert/dist/s-alert-css-effects/slide.css"; 20 | 21 | const App: React.FC = () => { 22 | return ( 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 | ); 44 | }; 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /client/src/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { 3 | Collapse, 4 | Navbar, 5 | NavbarToggler, 6 | NavbarBrand, 7 | Nav, 8 | NavItem, 9 | NavLink, 10 | UncontrolledDropdown, 11 | DropdownToggle, 12 | DropdownMenu, 13 | DropdownItem, 14 | Button 15 | } from "reactstrap"; 16 | import useLogin from "./hooks/useLogin"; 17 | import useViewer, { Query } from "./hooks/useViewer"; 18 | import ServerHelper, { ServerURL } from "./components/ServerHelper"; 19 | import { useCookies } from "react-cookie"; 20 | import { ClientSettings } from "./components/Types"; 21 | 22 | const AppHeader: React.FC = () => { 23 | const [isOpen, setIsOpen] = React.useState(false); 24 | const [cookies, setCookies] = useCookies(["settings"]); 25 | const settings: ClientSettings | null = cookies["settings"] 26 | ? cookies["settings"] 27 | : null; 28 | const { viewer, isLoggedIn } = useViewer(); 29 | const { logout } = useLogin(); 30 | 31 | const getSettings = async () => { 32 | const res = await ServerHelper.post(ServerURL.settings, {}); 33 | if (res.success) { 34 | setCookies("settings", res.settings, { path: "/" }); 35 | } 36 | }; 37 | 38 | document.title = (settings ? settings.app_name : "") + " HelpLIFO"; 39 | useEffect(() => { 40 | getSettings(); 41 | }, []); 42 | 43 | const viewerButton = isLoggedIn ? ( 44 | 45 | 46 | Hello {viewer(Query.name)} 47 | 48 | 49 | 50 | (window.location.href = "/profile")}> 51 | Profile 52 | 53 | 54 | Logout 55 | 56 | 57 | ) : null; 58 | return ( 59 |
60 | 65 | 66 | {settings ? settings.app_name : null} 67 | 68 | setIsOpen(open => !open)} /> 69 | 70 | 89 | 90 | 91 |
92 |
93 | ); 94 | }; 95 | 96 | export default AppHeader; 97 | -------------------------------------------------------------------------------- /client/src/Types.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /client/src/components/AdminPage.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { JsonEditor as Editor } from "jsoneditor-react"; 3 | // TODO(kevinfang): Upgrade to react-table v7 4 | import ReactTable from "react-table-6"; 5 | import "react-table-6/react-table.css"; 6 | import "jsoneditor-react/es/editor.min.css"; 7 | import React, { useState, useEffect } from "react"; 8 | import useLogin from "../hooks/useLogin"; 9 | import { 10 | CardBody, 11 | Card, 12 | CardTitle, 13 | Row, 14 | Col, 15 | Input, 16 | InputGroup, 17 | InputGroupAddon, 18 | InputGroupText 19 | } from "reactstrap"; 20 | import { Button, Tab } from "semantic-ui-react"; 21 | import ServerHelper, { ServerURL } from "./ServerHelper"; 22 | import createAlert, { AlertType } from "./Alert"; 23 | import { User, ClientSettings, AdminStats } from "./Types"; 24 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 25 | import { faStar } from "@fortawesome/free-solid-svg-icons"; 26 | 27 | const AdminPage = () => { 28 | document.body.classList.add("white"); 29 | const { getCredentials, logout } = useLogin(); 30 | const [settingsJSON, setSettingsJSON] = useState(null); 31 | const [adminStats, setAdminStats] = useState(null); 32 | const [data, setData] = useState([]); 33 | const [filteredData, setFilteredData] = useState([]); 34 | const [searchValue, setSearchValue] = useState(""); 35 | 36 | const promote = async (userID: string, type: string, value: boolean) => { 37 | const res = await ServerHelper.post(ServerURL.promoteUser, { 38 | ...getCredentials(), 39 | user_id: userID, 40 | type: type, 41 | value: value ? 1 : 0 42 | }); 43 | if (res.success) { 44 | setSettingsJSON(res.settings); 45 | setData(res.users); 46 | setAdminStats(res.ticket_stats); 47 | createAlert(AlertType.Success, "Updated User"); 48 | } else { 49 | createAlert(AlertType.Error, "Failed to update User"); 50 | } 51 | }; 52 | const columns = [ 53 | { 54 | Header: "Name", 55 | accessor: "name" 56 | }, 57 | { 58 | Header: "Email", 59 | accessor: "email" 60 | }, 61 | { 62 | Header: "Admin", 63 | accessor: "admin_is", 64 | Cell: (row: { original: User; value: string }) => ( 65 | <> 66 | 74 | 82 | 83 | ) 84 | } 85 | ]; 86 | 87 | const getData = async () => { 88 | const res = await ServerHelper.post(ServerURL.admin, getCredentials()); 89 | if (res.success) { 90 | setSettingsJSON(res.settings); 91 | setData(res.users); 92 | setAdminStats(res.ticket_stats); 93 | } else { 94 | createAlert( 95 | AlertType.Error, 96 | "Failed to get admin data, are you logged in?" 97 | ); 98 | } 99 | }; 100 | 101 | const updateData = async () => { 102 | const res = await ServerHelper.post(ServerURL.updateAdmin, { 103 | ...getCredentials(), 104 | data: JSON.stringify(settingsJSON) 105 | }); 106 | if (res.success) { 107 | setSettingsJSON(res.settings); 108 | setData(res.users); 109 | setAdminStats(res.ticket_stats); 110 | createAlert(AlertType.Success, "Updated JSON"); 111 | } else { 112 | createAlert(AlertType.Error, "Failed to update JSON"); 113 | } 114 | }; 115 | useEffect(() => { 116 | getData(); 117 | }, []); 118 | useEffect(() => { 119 | if (searchValue.length === 0) { 120 | setFilteredData(data); 121 | } else { 122 | const value = searchValue.toLowerCase(); 123 | // Searchable content 124 | setFilteredData( 125 | data.filter( 126 | (obj: User) => 127 | (obj.name && obj.name.toLowerCase().includes(value)) || 128 | (obj.email && obj.email.toLowerCase().includes(value)) || 129 | (obj.skills && obj.skills.toLowerCase().includes(value)) 130 | ) 131 | ); 132 | } 133 | }, [searchValue, data]); 134 | 135 | const panes = [ 136 | { 137 | menuItem: "Users", 138 | render: () => ( 139 | 140 | 141 |

Users

142 |
143 | setSearchValue(e.target.value)} 147 | /> 148 | 149 |
150 | ) 151 | }, 152 | { 153 | menuItem: "Stats", 154 | render: () => ( 155 | 156 | 157 |

Stats

158 |
159 |

160 | Average Wait:{" "} 161 | {adminStats && (adminStats.average_wait / 60).toFixed(1)} minutes 162 |
163 | Average Claimed Time:{" "} 164 | {adminStats && (adminStats.average_claimed / 60).toFixed(1)} minutes 165 |
166 | Average Rating: {adminStats && adminStats.average_rating.toFixed(2)} 167 |

168 |
169 | ) 170 | } 171 | ]; 172 | return ( 173 |
174 |

Admin Settings Page

175 |
176 | 177 | 178 | 179 | 180 | 181 |

Main Settings

182 |
183 |

184 | 185 | 186 | Mentor Login Link: 187 | 188 | e.target.select()} 196 | readOnly 197 | /> 198 | 199 |

200 | 208 | {settingsJSON ? ( 209 | 210 | ) : null} 211 |
    212 |
  • 213 | queue_status (whether the queue is on or off): true or{" "} 214 | false 215 |
  • 216 |
  • 217 | queue_message (if you want to send a message to everyone): 218 | empty or any string 219 |
  • 220 |
  • mentor_password_key (password mentors use to sign in!)
  • 221 |
  • app_creator (org running the program)
  • 222 |
  • app_name (name of event)
  • 223 |
  • app_contact_email (contact email for question)
  • 224 |
  • github_client_id (client id for github)
  • 225 |
  • github_client_secret (client secret for github)
  • 226 |
227 | 228 | 254 |
255 |
256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 |
265 |
266 | ); 267 | }; 268 | 269 | export default AdminPage; 270 | -------------------------------------------------------------------------------- /client/src/components/Alert.tsx: -------------------------------------------------------------------------------- 1 | import Alert from "react-s-alert"; 2 | 3 | export enum AlertType { 4 | Success, 5 | Warning, 6 | Error 7 | } 8 | 9 | const createAlert = (type: AlertType, message: string) => { 10 | switch (type) { 11 | case AlertType.Success: 12 | Alert.success(message, ALERT_SETTINGS); 13 | break; 14 | case AlertType.Warning: 15 | Alert.warning(message, ALERT_SETTINGS); 16 | break; 17 | case AlertType.Error: 18 | Alert.error(message, ALERT_SETTINGS); 19 | break; 20 | default: 21 | break; 22 | } 23 | }; 24 | const ALERT_SETTINGS = { 25 | position: "top-right", 26 | effect: "slide", 27 | beep: false, 28 | timeout: 3000, 29 | offset: 50 30 | }; 31 | 32 | export default createAlert; 33 | -------------------------------------------------------------------------------- /client/src/components/FAQPage.tsx: -------------------------------------------------------------------------------- 1 | import { settings as cluster_settings } from "cluster"; 2 | import React, { useState, useEffect } from "react"; 3 | import { Container, Button, Input, Label, Card, Form, Accordion } from "semantic-ui-react"; 4 | import useViewer from "../hooks/useViewer"; 5 | 6 | const FAQPage = () => { 7 | const { settings } = useViewer(); 8 | const panels = [ 9 | { 10 | key: 'q1', 11 | title: { 12 | content: 'How do I sign up as a hacker?', 13 | icon: 'star', 14 | }, 15 | content: [ 16 | 'Hackers have two ways to sign up: the first is through GitHub and the second is through DopeAuth, which only needs an email confirmation.', 17 | ].join(' '), 18 | }, 19 | { 20 | key: 'q2', 21 | title: { 22 | content: 'How do I sign up as a mentor?', 23 | icon: 'star', 24 | }, 25 | content: [ 26 | 'Mentors sign up using a mentor key that is shared prior to the event by an admin user of HelpQ.', 27 | ].join(' '), 28 | }, 29 | { 30 | key: 'q3', 31 | title: { 32 | content: 'What constitutes a good description of an issue?', 33 | icon: 'star', 34 | }, 35 | content: [ 36 | 'The more specific you are with describing your issue, the more prepared mentors will be in guiding you. A good description explains what technologies, tools, or frameworks you\'re using, includes relevant context of the project, and summarizes the fixes that your team has tried to work around the bug.' 37 | ] 38 | }, { 39 | key: 'q4', 40 | title: { 41 | content: 'What to put as the event in the ticket? ', 42 | icon: 'star', 43 | }, 44 | content: [ 45 | 'Select the track that you plan to submit to.' 46 | ] 47 | }, { 48 | key: 'q5', 49 | title: { 50 | content: 'I have trouble finding my mentor / hacker, where should I go? ', 51 | icon: 'star', 52 | }, 53 | content: [ 54 | 'Please reach out in the slack to hackers or mentors. If you are still having difficulties please reach out to a team member at blueprint@hackmit.org.' 55 | ] 56 | }, { 57 | key: 'q6', 58 | title: { 59 | content: 'What if I have more questions or need help using helpq? ', 60 | icon: 'star', 61 | }, 62 | content: [ 63 | 'Reach out to us in the slack or send us an email' + 64 | ((settings == null) ? '!' : ' at ' + settings.app_contact_email + '!') 65 | ] 66 | }, 67 | ] 68 | return ( 69 | 70 | 71 |

Frequently Asked Questions

72 | 77 |
78 |
79 | ); 80 | }; 81 | 82 | export default FAQPage; -------------------------------------------------------------------------------- /client/src/components/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Container, Button, Input, Card } from "semantic-ui-react"; 3 | import useLogin from "../hooks/useLogin"; 4 | import useViewer from "../hooks/useViewer"; 5 | import { RouteComponentProps } from "react-router"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faHeart } from "@fortawesome/free-solid-svg-icons"; 8 | 9 | const LandingPage = (props: RouteComponentProps) => { 10 | const { redirectToDopeAuth } = useLogin(); 11 | const { settings } = useViewer(); 12 | const search = new URLSearchParams(props.location.search); 13 | 14 | const [password, setPassword] = useState(search.get("key") || ""); 15 | const [isMentor, setIsMentor] = useState(password.length > 0); 16 | 17 | return ( 18 | 19 | 20 |

{settings ? settings.app_name : null} Help Queue

21 |

Presented by: {settings ? settings.app_creator : null}

22 | 23 |

The easy to use help queue system!

24 | {isMentor ? ( 25 | setPassword(e.target.value)} 30 | /> 31 | ) : null} 32 |
33 | {isMentor ? null : ( 34 | 37 | )} 38 | 48 | {!isMentor && settings && settings.github_client_id ? ( 49 | 59 | ) : null} 60 |
61 |
62 | 71 |
72 | ); 73 | }; 74 | 75 | export default LandingPage; 76 | -------------------------------------------------------------------------------- /client/src/components/LoginCallback.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RouteComponentProps } from "react-router"; 3 | import useLogin from "../hooks/useLogin"; 4 | import useViewer from "../hooks/useViewer"; 5 | import { join } from "path"; 6 | 7 | enum Status { 8 | start, 9 | loading, 10 | failed, 11 | succeed 12 | } 13 | 14 | const LoginCallback = (props: RouteComponentProps) => { 15 | const { isLoggedIn } = useViewer(); 16 | const [loginStatus, setLoginStatus] = React.useState( 17 | isLoggedIn ? Status.succeed : Status.start 18 | ); 19 | const search = new URLSearchParams(props.location.search); 20 | const { login } = useLogin(); 21 | React.useEffect(() => { 22 | const id = search.get("uid"); 23 | const token = search.get("token"); 24 | const email = search.get("email"); 25 | const mentor_key = search.get("mentor_key"); 26 | 27 | if (isLoggedIn) { 28 | window.location.replace("/profile"); 29 | } 30 | if ( 31 | id != null && 32 | token != null && 33 | email != null && 34 | loginStatus === Status.start 35 | ) { 36 | setLoginStatus(0); 37 | login(id, email, token, {"mentor_key": mentor_key}).then((success: Boolean) => { 38 | setLoginStatus(success ? Status.succeed : Status.failed); 39 | }); 40 | } else if (loginStatus === Status.start) { 41 | setLoginStatus(Status.failed); 42 | } 43 | // eslint-disable-next-line 44 | }, [isLoggedIn, loginStatus]); 45 | 46 | if (loginStatus === Status.start || loginStatus === Status.loading) { 47 | return

Attempting to login

; 48 | } else if (loginStatus === Status.succeed) { 49 | return

Successful Login

; 50 | } else { 51 | return

Login Failed

; 52 | } 53 | }; 54 | 55 | export default LoginCallback; 56 | -------------------------------------------------------------------------------- /client/src/components/LoginGithub.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { RouteComponentProps } from "react-router"; 3 | import useLogin from "../hooks/useLogin"; 4 | import useViewer from "../hooks/useViewer"; 5 | 6 | enum Status { 7 | start, 8 | loading, 9 | failed, 10 | succeed 11 | } 12 | 13 | const LoginGithub = (props: RouteComponentProps) => { 14 | const { isLoggedIn } = useViewer(); 15 | const [loginStatus, setLoginStatus] = React.useState( 16 | isLoggedIn ? Status.succeed : Status.start 17 | ); 18 | const search = new URLSearchParams(props.location.search); 19 | const { login } = useLogin(); 20 | React.useEffect(() => { 21 | const code = search.get("code"); 22 | const mentor_key = search.get("mentor_key"); 23 | 24 | if (isLoggedIn) { 25 | window.location.replace("/profile"); 26 | } 27 | if ( 28 | code != null && 29 | loginStatus === Status.start 30 | ) { 31 | setLoginStatus(0); 32 | login("GITHUB", "GITHUB", code, {"mentor_key": mentor_key, }).then((success: Boolean) => { 33 | setLoginStatus(success ? Status.succeed : Status.failed); 34 | }); 35 | } else if (loginStatus === Status.start) { 36 | setLoginStatus(Status.failed); 37 | } 38 | // eslint-disable-next-line 39 | }, [isLoggedIn, loginStatus]); 40 | 41 | if (loginStatus === Status.start || loginStatus === Status.loading) { 42 | return

Attempting to login

; 43 | } else if (loginStatus === Status.succeed) { 44 | return

Successful Login

; 45 | } else { 46 | return

Login Failed

; 47 | } 48 | }; 49 | 50 | export default LoginGithub; 51 | -------------------------------------------------------------------------------- /client/src/components/ProfilePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Container, Button, Input, Label, Card, Form } from "semantic-ui-react"; 3 | import useLogin from "../hooks/useLogin"; 4 | import { User } from "./Types"; 5 | import ServerHelper, { ServerURL } from "./ServerHelper"; 6 | import useViewer from "../hooks/useViewer"; 7 | import TagsInput from "react-tagsinput"; 8 | import "react-tagsinput/react-tagsinput.css"; // If using WebPack and style-loader. 9 | import { useCookies } from "react-cookie"; 10 | import createAlert, { AlertType } from "./Alert"; 11 | import { Alert, Badge } from "reactstrap"; 12 | 13 | const ProfilePage = () => { 14 | const { getCredentials } = useLogin(); 15 | const [_cookies, setCookie] = useCookies(); 16 | const { isLoggedIn } = useViewer(); 17 | const [user, setUser] = useState(null); 18 | const [name, setName] = useState(""); 19 | const [skills, setSkills] = useState([]); 20 | const [permissionsGranted, setPermissionsGranted] = useState(true); 21 | 22 | const getUser = async () => { 23 | const res = await ServerHelper.post(ServerURL.userTicket, getCredentials()); 24 | if (res.success) { 25 | setUser(res.user); 26 | setName(res.user.name || ""); 27 | } else { 28 | setUser(null); 29 | createAlert(AlertType.Error, "Failed to get user, are you logged in?"); 30 | } 31 | }; 32 | const saveProfile = async (shouldRedirect: string | null) => { 33 | if (name.length === 0) { 34 | createAlert(AlertType.Error, "Name must be nonempty"); 35 | return; 36 | } 37 | const res = await ServerHelper.post(ServerURL.userUpdate, { 38 | ...getCredentials(), 39 | name: name, 40 | affiliation: "", // TODO(kevinfang): add company affiliation 41 | skills: skills.join(";"), 42 | }); 43 | if (res.success) { 44 | setUser(res.user); 45 | createAlert(AlertType.Success, "Updated profile"); 46 | } else { 47 | setUser(null); 48 | createAlert(AlertType.Error, "Failed to update profile"); 49 | } 50 | if (shouldRedirect) { 51 | window.location.href = shouldRedirect; 52 | } 53 | }; 54 | 55 | useEffect(() => { 56 | getUser(); 57 | if (Notification) { 58 | Notification.requestPermission(); 59 | } 60 | }, []); 61 | const permission = Notification && Notification.permission; 62 | 63 | useEffect(() => { 64 | if (Notification && permission !== "granted") { 65 | setPermissionsGranted(false); 66 | } 67 | }, [permission]); 68 | 69 | const tempName = user ? user.name : null; 70 | const tempSkills = user ? user.skills : null; 71 | useEffect(() => { 72 | if (tempName) { 73 | setName(tempName); 74 | setCookie("name", tempName); 75 | } 76 | if (tempSkills) { 77 | setSkills(tempSkills.split(";").filter((e) => e.length > 0)); 78 | } 79 | }, [tempName, tempSkills]); 80 | 81 | if (!isLoggedIn) { 82 | window.location.href = "/login"; 83 | } 84 | 85 | if (!user) { 86 | return ( 87 |
88 |

Loading user...

89 |
90 | ); 91 | } 92 | 93 | return ( 94 | 95 | 96 |

Profile

97 |
98 | 99 | setName(e.target.value)} 103 | /> 104 | 105 | 106 | 113 | 114 |
115 |
116 | {user.mentor_is ? ( 117 | <> 118 | 122 | 123 | ) : null} 124 |
125 | {!permissionsGranted 126 | ? You have not enabled desktop notifications! Consider enabling them! Look at the top left corner 127 | : null} 128 |
129 | 130 | {!user.mentor_is || user.admin_is ? ( 131 | 139 | ) : null} 140 | {user.mentor_is || user.admin_is ? ( 141 | 149 | ) : null} 150 |
151 |
152 |
153 | ); 154 | }; 155 | 156 | export default ProfilePage; 157 | -------------------------------------------------------------------------------- /client/src/components/QueueMentor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Container, Button, Card, Select } from "semantic-ui-react"; 3 | import useLogin from "../hooks/useLogin"; 4 | import ServerHelper, { ServerURL } from "./ServerHelper"; 5 | import { Ticket } from "./Types"; 6 | import createAlert, { AlertType } from "./Alert"; 7 | import { Row, Col, Badge } from "reactstrap"; 8 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 9 | import { faStar } from "@fortawesome/free-solid-svg-icons"; 10 | import useViewer from "../hooks/useViewer"; 11 | 12 | const QueueMentor = () => { 13 | const { getCredentials } = useLogin(); 14 | const { settings } = useViewer(); 15 | const [tickets, setTickets] = useState(null); 16 | const [rankings, setRankings] = useState([]); 17 | const [ticket, setTicket] = useState(null); 18 | const [queueLength, setQueueLength] = useState(0); 19 | const locationOptions = [ 20 | { key: "", value: "no location", text: "No filter" }, 21 | ].concat( 22 | ((settings && settings.locations) || "no location") 23 | .split(",") 24 | .map((l) => ({ key: l, value: l, text: l })) 25 | ); 26 | const [filteredTickets, setFilteredTickets] = useState([]); 27 | const [searchValue, setSearchValue] = useState("no location"); 28 | 29 | const getTickets = async () => { 30 | const res = await ServerHelper.post( 31 | ServerURL.userTickets, 32 | getCredentials() 33 | ); 34 | document.title = 35 | `(${res.tickets && res.tickets.length}) ` + 36 | (settings ? settings.app_name : "") + 37 | " Help Queue"; 38 | if (res.success) { 39 | setTickets(res.tickets); 40 | setRankings(res.rankings); 41 | setTicket(res.ticket); 42 | setQueueLength(res.queue_length); 43 | if (!res.user.mentor_is) { 44 | createAlert( 45 | AlertType.Error, 46 | "You are not registered as a mentor! Ask your admin to whitelist you" 47 | ); 48 | } 49 | } else { 50 | setTickets(null); 51 | } 52 | }; 53 | 54 | useEffect(() => { 55 | if (!tickets) return; 56 | if (searchValue === "no location") { 57 | setFilteredTickets(tickets); 58 | return; 59 | } 60 | setFilteredTickets( 61 | tickets.filter((ticket) => ticket.data.location.includes(searchValue)) 62 | ); 63 | }, [searchValue, tickets]); 64 | 65 | useEffect(() => { 66 | // On load check to see what the status is of the ticket 67 | getTickets(); 68 | 69 | const interval = setInterval(getTickets, 5000); 70 | return () => clearInterval(interval); 71 | }, []); 72 | 73 | let queueCard = null; 74 | if (ticket == null || ticket.status != 1) { 75 | // If the mentor version hasn't claimed the ticket 76 | if (tickets == null || queueLength == 0) { 77 | queueCard =

There are no tickets :(

; 78 | } else { 79 | queueCard = filteredTickets.map((ticket) => { 80 | return ( 81 | 82 |

83 | {ticket.requested_by} asked {ticket.minutes}{" "} 84 | mins ago: 85 |

86 |

{ticket.data.question}

87 | {ticket.data.location !== "no location" && 88 | ticket.data.location !== "default" ? ( 89 | 90 | {ticket.data.location} 91 | 92 | ) : null} 93 | 110 |
111 | ); 112 | }); 113 | } 114 | } else if (ticket != null) { 115 | // Ticket has been claimed 116 | queueCard = ( 117 | <> 118 |

119 | You have claimed: {ticket.requested_by}{" "} 120 |

121 |

122 | Question: {ticket.data.question} 123 |
124 | Location: {ticket.data.location} 125 |
126 | Contact: {ticket.data.contact} 127 |

128 |

129 | {settings && 130 | settings.jitsi_link && 131 | settings.jitsi_link.includes("://") ? ( 132 | 133 | {settings.jitsi_link + "/" + ticket.uid} 134 | 135 | ) : null} 136 |

137 |
138 | 154 | 170 |
171 | 172 | ); 173 | } 174 | 175 | return ( 176 | 177 | 178 | 0 ? "8" : "12"}> 179 | 180 |

Mentor Queue

181 |

Queue length: {queueLength}

182 |