├── .babelrc ├── .gitignore ├── CodeHuddle.xml ├── README.md ├── app ├── Routes.jsx ├── components │ ├── Canvas.jsx │ ├── Editor.jsx │ ├── FeedbackCandidate.jsx │ ├── InterviewList.jsx │ ├── InterviewPlanning.jsx │ ├── InterviewRoomOptions.jsx │ ├── InterviewerDashboard.jsx │ ├── Jokes.jsx │ ├── Jokes.test.jsx │ ├── Login.jsx │ ├── Login.test.jsx │ ├── RateProblem.jsx │ ├── RoomSelectBtn.jsx │ ├── Signup.js │ ├── WhiteboardConstants.jsx │ ├── WhiteboardContainer.jsx │ ├── WhiteboardToolbar.jsx │ ├── WhoAmI.jsx │ ├── WhoAmI.test.jsx │ ├── interview-room │ │ └── InterviewRoom.jsx │ └── splash │ │ ├── Home.jsx │ │ ├── Splash-Content.jsx │ │ ├── Splash-Footer.jsx │ │ ├── Splash-Nav-1.jsx │ │ ├── Splash-Nav.jsx │ │ ├── Splash.jsx │ │ └── splash-content │ │ ├── SC-content-1.jsx │ │ ├── SC-content-2.jsx │ │ ├── SC-content-3.jsx │ │ └── SC-header.jsx ├── main.jsx ├── reducers │ ├── allInterviews.jsx │ ├── auth.jsx │ ├── editor.jsx │ ├── index.jsx │ ├── interviewInfo.jsx │ ├── interviewPlanningInfo.jsx │ ├── interviewProblems.jsx │ └── whiteboard.jsx ├── sockets.js ├── store.jsx └── utils.js ├── bin ├── build-branch.sh ├── deploy-heroku.sh ├── mkapplink.js └── setup ├── codehuddle.sketch ├── db ├── index.js ├── models │ ├── index.js │ ├── interview.js │ ├── interviewProblem.js │ ├── oauth.js │ ├── organization.js │ ├── problem.js │ ├── solution.js │ ├── user.js │ └── user.test.js └── seed.js ├── index.js ├── node_modules └── APP ├── package.json ├── public ├── _ir-style.scss ├── animate.css ├── favicon.ico ├── images │ ├── ace.png │ ├── brush.svg │ ├── cloud-sync.svg │ ├── code-background.jpeg │ ├── code-outline.svg │ ├── developerpicture.jpg │ └── hired.jpg ├── index.html ├── style.scss └── stylesheets │ ├── style.css │ └── style.css.map ├── server ├── api.js ├── auth.filters.js ├── auth.js ├── auth.test.js ├── bonesAuth.js ├── bonesUsers.test.js ├── interviewProblems.js ├── interviews.js ├── interviews.test.js ├── organizations.js ├── organizations.test.js ├── problems.js ├── problems.test.js ├── redux │ ├── reducers │ │ └── interview.js │ └── store.js ├── sockets.js ├── start.js ├── users.js └── users.test.js ├── utils.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-2"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all node_modules 2 | node_modules/* 3 | 4 | # ...except the symlink to ourselves. 5 | !node_modules/APP 6 | 7 | # Compiled JS 8 | public/bundle.js 9 | public/bundle.js.map 10 | 11 | # NPM errors 12 | npm-debug.log 13 | 14 | # .sass-cache 15 | .sass-cache/* 16 | -------------------------------------------------------------------------------- /CodeHuddle.xml: -------------------------------------------------------------------------------- 1 | 7Vxbb+I4FP41SLMPHeUK4XGgndmRumrVarW7jyYxwTOOzTqmlP76tYlNLk4ghUBnRdqqSk4cX853js/NMHCnyes3BpaLP2gE8cCxoteBeztwnMDzxH9J2GQEbxRkhJihKCPZOeEZvUFFtBR1hSKYlhpySjFHyzIxpITAkJdogDG6LjebU1wedQliaBCeQ4BN6l8o4gu1LN/K6b9DFC/0yLalnsxA+DNmdEXUeAPHnW9/sscJ0H2p9ukCRHRdILl3A3fKKOXZVfI6hViyVrMte+9rw9PdvBkkvM0LTvbCC8ArtfQ/U8jU3PhG8yNdowQDIu4mC55gQbTF5ZwS/qwaWeI+XCAc3YMNXcmxUy54oe8mC8rQm2gP9MviMeMKeWcoe0MYTymmbDukCy35W3rzWfaoxmIwFe8+6oXaO9I9SLmeD8UYLFM0285QNkkAixGZUM5pohrppX0tD69QcycAo5gIWigGEpxxJyaLFddfIOPwtUBSLP8GaQI524gm6ulIoa+0I1AorHNRs4eqyaIoZoEWcSXe8a7nHGJxoVCuR9w1ESfoX3HtWHcJQNjAXqyJl3FPOaM/oWYWoVu5KMKnSOkShIjE93AuF+DllCe1JklaLxCHz4Iuh1uL3UTQqGDlHG/1YoGiCBKJLuWAg9lO2JYUEb7lgz8Rf4JdU+uzP/DFlKfi3s7vxZ9szviUEjF1gLa4QSEnayhlpQZRpzWiCkIhwa0gdLzTEdSbZAHCR0YFZ5K0V9zzKq4fVDR33FZzgw5wtw3clxnu0tLCNGRoyRElvQYXlaR7FR52AKVpd58pXkn0TB3u4WsDn9/SiHYBn29q4i2az1G4whzLOd3DF9hb0uOA1KHCISBdpwNnyHLgKBCA+rMZtGfOzchEdoJiQXjo0TwKTVu/cgk4R1Zoz8Fs5s5GEFhucOOb3u4DiwFBbyCzlJbAxbHICuNPy9UMo/C3q8XZwLQO+macvbb7r3sOta2xp4JdvS09UmmH/uXA9IYGdjIHQUACe/iOCVIqMYrttjSoXXhG3sgA87sMv14QXBto9sFpp8HpTtF1QDNuC7zOd5yE/NhAfgpIhCLAez0uqcYvGZH6ZlJpp7g1CeEewDYAXjImNX0i30A000XRlcVRb12PBLVtfNqJVprxyw86k/htljJjLzsS66Ep6hN/RwNqW97lEDVDVM+AWDgxfCXG+CKXJrwIAqNMayMJzrWCfCA+zXBvBtltmYc4C8gj07qGuXNkMcAF10WvIU0SSK44bj0RZP+CHpMB8tAMZPP6DEZilT2qR6Hatj5+FjeqDlWQpmvKoqvFs9P0hN+yhnoedEcGiDCKoc4/CE4saEwJwHc5dbI93wMjxd0C2vAV8b8lWXAzu/tHPyFipoVH8lY/+wE536icBVhxKlHdjXtP6VK1a+R9SlcsVLNXqsIBiyEvkeS69uLDIBZ26AWW+u6W24GhS4zi63VpTtMjp6xHwQUdHDPYNAOVvYoF8YyuizpV1CISfZFn+MTt3VMi3KSHDFqZ2qt7sNO6gs5latbI5YoulFUoy2SZfC8y1qph7I7YWpHUGI9SrnJg3cArIetqFHUX2WTVWzloRkeVY2a7/KDuJ9skjH4Ej8Gm0EyJfeN8nWrasXTkUFxkHeaCtePpcbIWmImN/62s+Q1VNcXam11eTjN3VKPle4Wxvdx54/JYbtCR3HnnkTuvMl3n3HK3t7ZhPe58/fs6X78vdnR7Eq+Kvt22ZNlCMZ5gyAGJpWeixxu6FaH2zfGCFtYWYC5rmxxOpBOZGkb3VBk1Paw8Bv1+23taexX7l6zMmNM1K23FfahHuSuUP7R8MzZPopUKctaTShpa23NMhVqrtRSsp0w4LWEfWnUkCh96KFH7ZfWl9Yog5Nv9J7o9Dg7w9Z5p61gKPvYw49Dc93OwVc1IoF6qGK1SfUUoL90zKHeS6Hpl40BSukFcTj8AeZ4twqwiGsCWYmLCEd88bRMVlHQcHOtUaB4c5+nPYvLzQOBcjJMbiy3FRGejp3Qw9amd/MoJKtuzTUw7iraHtlc/1nujbbeSIvCscbmjhnD7iHDDLF6bxZHy+eo+CD4QBB86db1XXt1KEBy0DEs6+TiaKQxmfuSqD/EegLZBmX6JmNPcbt+b3z+LfdljRZptT60JeL/B0dJdNDgNnzf5wNJBJRd/dArXqeSC29YOujAqNZ/B2i9r78jvb2rla9POebFNsTtOuBrTdkXhshv2/kvJ0rCSxve0Bry7HFDpyK121FE9oDqOM+y2IGAKqnOBTfGwxFqfx55XkNob67M9dhThETIk1ig9ju222ZHADhts1aWEs1oXc62K1WvtPVdcKFufxzx5oxO3+fe4ZM3z78px7/4D -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Code Huddle Logo](http://i.imgur.com/j8EBw4u.png "Code Huddle") 2 | --- 3 | ![code ship](https://codeship.com/projects/4f55ecf0-c4a3-0134-b0f8-4e6f0fe654b5/status?branch=master) 4 | 5 | A single page application for conducting coding interviews. This application features a collaborative coding and whiteboard environment. Built as a capstone project for the Fullstack Academy Software Engineering Immersive program. 6 | 7 | [Live demo](https://codehuddle.herokuapp.com) 8 | 9 | ## Installation 10 | 11 | First, clone the repository through git and change to the new directory: 12 | ``` 13 | git clone https://github.com/CodehuddleFSA/codehuddle.git 14 | cd codehuddle 15 | ``` 16 | 17 | Then install the required dependencies: 18 | ``` 19 | npm install 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Starting 25 | To start the application locally, first start your Postgres server, then run the following command: 26 | ``` 27 | npm start 28 | ``` 29 | The application will be available locally at `http://localhost:1337/`. 30 | 31 | ### Building 32 | While in development, you can build using: 33 | ``` 34 | npm run build-watch 35 | npm run build-sass 36 | ``` 37 | 38 | ### Seeding 39 | You can populate the database using: 40 | ``` 41 | npm run seed 42 | ``` 43 | 44 | ### Testing 45 | You can test the application using: 46 | ``` 47 | npm test 48 | ``` 49 | 50 | ## Screenshots 51 | #### Coding interview page with collaborative editor and whiteboard 52 | ![Code Huddle Screen Shot 1](http://imgur.com/XNBoJOL.jpg "Code Huddle") 53 | #### Interviewer dashboard, displays all the interviews 54 | ![Code Huddle Screen Shot 1](http://imgur.com/HaFRxyq.jpg "Code Huddle") 55 | #### Interview planning, allows the interviewer to edit the interview details and problems 56 | ![Code Huddle Screen Shot 1](http://imgur.com/IVWlECR.jpg "Code Huddle") 57 | 58 | ## License 59 | MIT © CodehuddleFSA 60 | -------------------------------------------------------------------------------- /app/Routes.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Required libraries 4 | import React from 'react'; 5 | import { browserHistory, Router, Route } from 'react-router'; 6 | import { connect } from 'react-redux'; 7 | 8 | // Required files 9 | import Login from './components/Login'; 10 | import WhoAmI from './components/WhoAmI'; 11 | import Home from './components/splash/Home'; 12 | import InterviewRoom from './components/interview-room/InterviewRoom'; 13 | import FeedbackCandidate from './components/FeedbackCandidate'; 14 | import InterviewerDashboard from './components/InterviewerDashboard'; 15 | import InterviewPlanning from './components/InterviewPlanning'; 16 | import Signup from './components/Signup'; 17 | 18 | // Helper functions 19 | import { socketsJoinRoom } from 'APP/app/sockets'; 20 | import { fetchProblems } from 'APP/app/reducers/interviewProblems'; 21 | import { fetchAllInterviews } from 'APP/app/reducers/allInterviews'; 22 | import { fetchInterview } from 'APP/app/reducers/interviewInfo'; 23 | 24 | /* ----------------- COMPONENT ------------------ */ 25 | const Routes = ({ interviewOnEnter, feedbackCandidateOnEnter, interviewDashboardOnEnter, interviewPlanningOnEnter }) => ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | 37 | /* ----------------- CONTAINER ------------------ */ 38 | 39 | const mapProps = (state) => ({ 40 | user: state.auth 41 | }); 42 | 43 | const mapDispatch = (dispatch, ownProps) => ({ 44 | feedbackCandidateOnEnter: (nextState) => { 45 | dispatch(fetchProblems(nextState.params.interviewID)); 46 | }, 47 | interviewOnEnter: (nextState) => { 48 | socketsJoinRoom(nextState.params.room); 49 | }, 50 | interviewDashboardOnEnter: (nextState) => { 51 | dispatch(fetchAllInterviews(nextState.params.userID)); 52 | }, 53 | interviewPlanningOnEnter: (nextState) => { 54 | dispatch(fetchInterview(nextState.params.interviewID)); 55 | dispatch(fetchProblems(nextState.params.interviewID)); 56 | } 57 | }); 58 | 59 | export default connect(mapProps, mapDispatch)(Routes); 60 | -------------------------------------------------------------------------------- /app/components/Canvas.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | const { Component } = React; 4 | import { findDOMNode } from 'react-dom'; 5 | import { connect } from 'react-redux'; 6 | 7 | /* ----------------- COMPONENT ------------------ */ 8 | 9 | class Canvas extends Component { 10 | componentDidMount () { 11 | // Set the canvas context 12 | const canvas = findDOMNode(this.refs.canvas); 13 | const ctx = canvas.getContext('2d'); 14 | this.props.initCanvas(ctx); 15 | 16 | // Initialize default state for canvas 17 | const currentMousePosition = { x: 0, y: 0 }; 18 | const lastMousePosition = { x: 0, y: 0 }; 19 | let drawing = false; 20 | 21 | const self = this; // :( 22 | canvas.addEventListener('mousedown', function (evt) { 23 | drawing = true; 24 | // Update current mouse position 25 | currentMousePosition.x = evt.pageX - this.offsetLeft; 26 | currentMousePosition.y = evt.pageY - this.offsetTop; 27 | }); 28 | 29 | canvas.addEventListener('mouseup', function () { 30 | drawing = false; 31 | }); 32 | 33 | canvas.addEventListener('mousemove', function (evt) { 34 | if (!drawing) return; // Short circuit if mouse button isn't down 35 | 36 | // Update last and current mouse positions 37 | lastMousePosition.x = currentMousePosition.x; 38 | lastMousePosition.y = currentMousePosition.y; 39 | currentMousePosition.x = evt.pageX - this.offsetLeft; 40 | currentMousePosition.y = evt.pageY - this.offsetTop; 41 | 42 | // Send dispatch out for new coordinates 43 | self.props.setCoordinates(lastMousePosition, currentMousePosition, '#000000'); 44 | }); 45 | } 46 | 47 | componentDidUpdate () { 48 | const ctx = this.props.ctx; // Grab the canvas context 49 | if (ctx.notReady) return; 50 | 51 | const drawingHistory = this.props.drawingHistory; 52 | 53 | if (drawingHistory.length) { // If there is a drawing history, draw then clear 54 | drawingHistory.forEach(event => { 55 | draw(event.lastPx, event.currentPx, event.color); 56 | }); 57 | this.props.clearHistory(); 58 | return; 59 | } 60 | 61 | // Else, on every re-render, draw the new line 62 | const { lastPx, currentPx, color } = this.props.lastDraw; 63 | draw(lastPx, currentPx, color); 64 | 65 | function draw (lastPx, currentPx, color) { 66 | ctx.beginPath(); 67 | ctx.strokeStyle = color; 68 | ctx.moveTo(lastPx.x, lastPx.y); 69 | ctx.lineTo(currentPx.x, currentPx.y); 70 | ctx.closePath(); 71 | ctx.stroke(); 72 | } 73 | } 74 | 75 | render () { 76 | return ( 77 | 78 | ); 79 | } 80 | } 81 | 82 | /* ----------------- CONTAINER ------------------ */ 83 | 84 | import { initCanvas, setCoordinates, clearHistory } from '../reducers/whiteboard'; 85 | 86 | const mapStateToProps = (state) => { 87 | return { 88 | lastDraw: state.interview.whiteboard.get('lastDraw').toJS(), 89 | ctx: state.interview.whiteboard.get('ctx'), 90 | drawingHistory: state.interview.whiteboard.get('drawingHistory') 91 | }; 92 | }; 93 | 94 | const mapDispatchToProps = (dispatch) => { 95 | return { 96 | initCanvas: (ctx) => { 97 | dispatch(initCanvas(ctx)); 98 | }, 99 | setCoordinates: (lastPx, currentPx, color) => { 100 | dispatch(setCoordinates(lastPx, currentPx, color)); 101 | }, 102 | clearHistory: () => { 103 | dispatch(clearHistory()); 104 | } 105 | }; 106 | }; 107 | 108 | export default connect(mapStateToProps, mapDispatchToProps)(Canvas); 109 | 110 | -------------------------------------------------------------------------------- /app/components/Editor.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import AceEditor from 'react-ace'; 4 | 5 | import 'brace/mode/javascript'; 6 | import 'brace/theme/github'; 7 | import 'brace/theme/solarized_dark'; 8 | import 'brace/theme/tomorrow'; 9 | import 'brace/theme/clouds'; 10 | import 'brace/mode/plain_text'; 11 | 12 | // Required files 13 | import { parseToMarker } from 'APP/app/utils'; 14 | 15 | /* ----------------- COMPONENT ------------------ */ 16 | 17 | const editorProps = { 18 | autoScrollEditorIntoView: false, 19 | $blockScrolling: Infinity 20 | } 21 | 22 | export const Editor = ({ onChange, text, options, ranges, setRange, onChangeSelection }) => { 23 | return ( 24 | 40 | ); 41 | }; 42 | 43 | /* ----------------- CONTAINER ------------------ */ 44 | 45 | // Required libraries 46 | import {connect} from 'react-redux'; 47 | 48 | // Required files 49 | import { setText } from '../reducers/editor'; 50 | import { setRange } from 'APP/app/reducers/editor'; 51 | 52 | const mapState = (state) => { 53 | return { 54 | text: state.interview.editor.get('text'), 55 | options: state.interview.editor.get('options').toJS(), 56 | ranges: state.interview.editor.get('ranges').toJS() 57 | }; 58 | }; 59 | 60 | const mapDispatch = (dispatch) => { 61 | return { 62 | onChange: text => { 63 | dispatch(setText(text)); 64 | }, 65 | setRange: range => { 66 | dispatch(setRange(range)); 67 | }, 68 | onChangeSelection: editor => { 69 | if (editor.$mouseHandler.isMousePressed) { 70 | const currentRange = editor.selection.getRange(); 71 | const parsedRange = { 72 | start: currentRange.start, 73 | end: currentRange.end 74 | }; 75 | 76 | dispatch(setRange(parsedRange)); 77 | } 78 | } 79 | }; 80 | }; 81 | 82 | export default connect(mapState, mapDispatch)(Editor); 83 | -------------------------------------------------------------------------------- /app/components/FeedbackCandidate.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import Rating from 'react-rating'; 4 | 5 | // Required files 6 | import RateProblem from './RateProblem'; 7 | 8 | /* ----------------- COMPONENT ------------------ */ 9 | 10 | export const FeedbackCandidate = ({ interviewProblems }) => { 11 | return ( 12 |
13 |

We strive to deliver the best queality interviews. Please give us some feedback on our problems.

14 | { 15 | interviewProblems.map((problem, idx) => { 16 | // const { problem_id, interview_id } = problem.interviewProblems; 17 | return ( 18 | 22 | ); 23 | }) 24 | } 25 |
26 | ); 27 | }; 28 | 29 | /* ----------------- CONTAINER ------------------ */ 30 | 31 | // Required libraries 32 | import { connect } from 'react-redux'; 33 | 34 | // Required files 35 | 36 | const mapState = (state) => { 37 | return { 38 | interviewProblems: state.interview.interviewProblems.toJS() 39 | }; 40 | }; 41 | 42 | const mapDispatch = (dispatch) => { 43 | return { 44 | }; 45 | }; 46 | 47 | export default connect(mapState, mapDispatch)(FeedbackCandidate); 48 | -------------------------------------------------------------------------------- /app/components/InterviewList.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | 4 | // Required files 5 | import { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn } from 'material-ui/Table'; 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | 8 | 9 | /* ----------------- COMPONENT ------------------ */ 10 | 11 | export const InterviewList = ({ allInterviews }) => { 12 | return ( 13 |
14 |

Your interviews:

15 | 16 | 20 | 21 | ID 22 | Date 23 | Name 24 | Position 25 | Status 26 | 27 | 28 | 29 | 33 | { allInterviews.map(interview => { 34 | return ( 35 | 36 | { interview.id } 37 | { interview.date && new Date(interview.date).toLocaleDateString() } 38 | { interview.candidateName } // TODO: change this to candidate name 39 | { interview.position } 40 | { interview.status } 41 | 42 | 48 | 49 | 50 | ) 51 | }) } 52 | 53 |
54 |
55 | ); 56 | }; 57 | 58 | /* ----------------- CONTAINER ------------------ */ 59 | 60 | // Required libraries 61 | import { connect } from 'react-redux'; 62 | 63 | // Required files 64 | 65 | // TODO: look for Immutable method instead of handing down a normal method 66 | const mapState = (state) => { 67 | return { 68 | allInterviews: state.allInterviews.toJS() 69 | }; 70 | }; 71 | 72 | const mapDispatch = (dispatch) => { 73 | return { 74 | }; 75 | }; 76 | 77 | export default connect(mapState, mapDispatch)(InterviewList); 78 | -------------------------------------------------------------------------------- /app/components/InterviewPlanning.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import Immutable from 'immutable'; 4 | import TextField from 'material-ui/TextField'; 5 | import AppBar from 'material-ui/AppBar'; 6 | import DatePicker from 'material-ui/DatePicker'; 7 | import TimePicker from 'material-ui/TimePicker'; 8 | import RaisedButton from 'material-ui/RaisedButton'; 9 | import {Card, CardActions, CardHeader, CardMedia, CardTitle, CardText} from 'material-ui/Card'; 10 | import {Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn} from 'material-ui/Table'; 11 | import Dialog from 'material-ui/Dialog'; 12 | import FlatButton from 'material-ui/FlatButton'; 13 | import FloatingActionButton from 'material-ui/FloatingActionButton'; 14 | import ContentAdd from 'material-ui/svg-icons/content/add'; 15 | import ActionDelete from 'material-ui/svg-icons/action/delete'; 16 | const style = { 17 | margin: 12, 18 | }; 19 | const removeButtonStyle = { 20 | marginRight: 20 21 | }; 22 | 23 | 24 | // ----------------InterviewPlanning Container-------------- // 25 | 26 | export class InterviewPlanning extends React.Component { 27 | constructor (props) { 28 | super(props); 29 | const {candidateName, candidateEmail, date, time, position} = props.selectedInterviewInfo; 30 | let changeableProblemSet = props.selectedInterviewProblems; 31 | this.state = { 32 | candidateName, 33 | candidateEmail, 34 | interviewDate: date, 35 | interviewTime: time, 36 | position: position, 37 | selectedProblems: props.selectedInterviewProblems, 38 | user: props.user, 39 | showOrganizationProblemSet: false 40 | 41 | }; 42 | 43 | this.handleDateChange = this.handleDateChange.bind(this); 44 | this.handleChangeTimePicker24 = this.handleChangeTimePicker24.bind(this); 45 | this.handlePositionChange = this.handlePositionChange.bind(this); 46 | this.handleAddProblems = this.handleAddProblems.bind(this); 47 | this.handleNameChange = this.handleNameChange.bind(this); 48 | this.handleRemoveProblem = this.handleRemoveProblem.bind(this); 49 | this.handleAddProblemToInterview = this.handleAddProblemToInterview.bind(this); 50 | this.handleOrganizationProblemSetClose = this.handleOrganizationProblemSetClose.bind(this); 51 | } 52 | 53 | componentWillReceiveProps(nextProps) { 54 | if (this.props !== nextProps) { 55 | this.setState({ 56 | candidateName: nextProps.selectedInterviewInfo.candidateName, 57 | candidateEmail: nextProps.selectedInterviewInfo.candidateEmail, 58 | interviewDate: new Date(nextProps.selectedInterviewInfo.date), 59 | interviewTime: new Date(nextProps.selectedInterviewInfo.date), 60 | position: nextProps.selectedInterviewInfo.position, 61 | selectedProblems: nextProps.selectedInterviewProblems, 62 | user: nextProps.user, 63 | height: '200px', 64 | fixedHeader: true, 65 | fixedFooter: true, 66 | stripedRows: false, 67 | showRowHover: false, 68 | selectable: true, 69 | multiSelectable: false, 70 | enableSelectAll: false, 71 | deselectOnClickaway: true, 72 | showCheckboxes: true, 73 | }); 74 | } 75 | } 76 | 77 | handleNameChange(event, name) { 78 | this.setState({ 79 | candidateName: name 80 | }); 81 | } 82 | 83 | handleDateChange(event, date) { 84 | this.setState({ 85 | interviewDate: date 86 | }); 87 | } 88 | 89 | handleChangeTimePicker24 (event, time) { 90 | this.setState({ 91 | interviewTime: time}); 92 | } 93 | 94 | handlePositionChange (event, position) { 95 | this.setState({ 96 | position: position}); 97 | } 98 | 99 | handleRemoveProblem (i){ 100 | let tempProblem = this.state.selectedProblems.splice(i, 1); 101 | this.setState({ 102 | selectedProblems: this.state.selectedProblems 103 | }); 104 | } 105 | 106 | handleAddProblemToInterview (j){ 107 | this.state.selectedProblems.push(this.props.problems[j]); 108 | this.setState({ 109 | selectedProblems: this.state.selectedProblems 110 | }); 111 | } 112 | 113 | handleOrganizationProblemSetClose() { 114 | this.setState({ 115 | showOrganizationProblemSet: false 116 | }); 117 | } 118 | 119 | handleAddProblems (evt) { 120 | evt.preventDefault(); 121 | this.setState({ 122 | showOrganizationProblemSet: true 123 | }); 124 | this.props.receiveProblems(this.props.user.organization_name); 125 | } 126 | 127 | handleSaveInterview (evt) { 128 | evt.preventDefault(); 129 | this.props.addInterview(this.state); 130 | } 131 | 132 | render () { 133 | return ( 134 |
135 | 136 |
137 | 138 | 139 | 140 | 141 | 142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
162 | 163 | 164 | 165 | 166 | 167 |
168 | 169 | 170 | 171 | Name 172 | Description 173 | Difficulty 174 | Remove 175 | 176 | 177 | 178 | {this.state.selectedProblems.map((p, i) => 179 | 180 | {p.name} 181 | {p.description} 182 | {p.difficulty} 183 | this.handleRemoveProblem(i)}> 184 | 185 | )} 186 | 187 |
188 |
189 | 190 | 191 |
192 | ]} 193 | modal={false} open={this.state.showOrganizationProblemSet} onRequestClose={this.handleOrganizationProblemSetClose} autoScrollBodyContent={true}> 194 | 195 | 196 | {this.props.problems.map((q, j) => 197 | 198 | {q.name} 199 | {q.description} 200 | {q.difficulty} 201 | this.handleAddProblemToInterview(j)}> 202 | 203 | )} 204 | 205 |
206 |
207 |
208 |
209 | ); 210 | } 211 | } 212 | 213 | /* ----------------- CONNECT CONTAINER ------------------ */ 214 | 215 | import { receiveProblems, addInterview } from '../reducers/interviewPlanningInfo'; 216 | 217 | const mapStateToProps = state => { 218 | return { 219 | user: state.auth, 220 | selectedInterviewInfo: state.interview.interviewInfo.toJS(), 221 | selectedInterviewProblems: state.interview.interviewProblems.toJS(), 222 | problems: state.interviewPlanning.problems 223 | }; 224 | }; 225 | 226 | const mapDispatchToProps = dispatch => { 227 | return { 228 | receiveProblems: organization => { 229 | dispatch(receiveProblems(organization)); 230 | }, 231 | addInterview: () => { 232 | dispatch(addInterview()); 233 | } 234 | }; 235 | }; 236 | 237 | export default connect(mapStateToProps, mapDispatchToProps)(InterviewPlanning); 238 | -------------------------------------------------------------------------------- /app/components/InterviewRoomOptions.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import Checkbox from 'material-ui/Checkbox'; 4 | import ActionFavorite from 'material-ui/svg-icons/action/favorite'; 5 | import ActionFavoriteBorder from 'material-ui/svg-icons/action/favorite-border'; 6 | import Visibility from 'material-ui/svg-icons/action/visibility'; 7 | import VisibilityOff from 'material-ui/svg-icons/action/visibility-off'; 8 | import Toggle from 'material-ui/Toggle'; 9 | 10 | /* ----------------- COMPONENT ------------------ */ 11 | 12 | const styles = { 13 | block: { 14 | padding: 20, 15 | maxWidth: 250 16 | }, 17 | checkbox: { 18 | marginBottom: 16 19 | } 20 | }; 21 | 22 | export const InterviewRoomOptions = ({ options, setOptions }) => { 23 | return ( 24 |
25 |
26 | setOptions(options.linting, "linting") } 30 | toggled={ options.linting } 31 | /> 32 | 33 | setOptions(options.showGutter, "showGutter") } 37 | toggled={ options.showGutter } 38 | /> 39 | 40 | setOptions(options.textSize, "textSize") } 44 | toggled={ options.textSize } 45 | /> 46 | 47 | setOptions(options.theme, "theme") } 51 | toggled={ options.theme } 52 | /> 53 |
54 |
55 | ); 56 | }; 57 | 58 | /* ----------------- CONTAINER ------------------ */ 59 | 60 | // Required libraries 61 | import { connect } from 'react-redux'; 62 | 63 | // Required files 64 | import { setOptions, parseEvt } from '../reducers/editor'; 65 | 66 | const mapState = (state) => { 67 | return { 68 | // options: state.interview.editor.options 69 | options: state.interview.editor.get('options').toJS() 70 | }; 71 | }; 72 | 73 | const mapDispatch = (dispatch) => { 74 | return { 75 | setOptions: (checked, name) => { 76 | dispatch(setOptions(parseEvt(!checked, name))); 77 | } 78 | }; 79 | }; 80 | 81 | export default connect(mapState, mapDispatch)(InterviewRoomOptions); 82 | -------------------------------------------------------------------------------- /app/components/InterviewerDashboard.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import FloatingActionButton from 'material-ui/FloatingActionButton'; 4 | import ContentAdd from 'material-ui/svg-icons/content/add'; 5 | 6 | // Required files 7 | import InterviewList from './InterviewList'; 8 | 9 | /* ----------------- COMPONENT ------------------ */ 10 | 11 | export const InterviewerDashboard = ({ interviewProblems, user, createInterviewAndRedirect }) => { 12 | return ( 13 |
14 |

Welcome, { user && user.name }

15 | 16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | /* ----------------- CONTAINER ------------------ */ 24 | 25 | // Required libraries 26 | import { connect } from 'react-redux'; 27 | 28 | // Required files 29 | import { createInterviewAndRedirect } from 'APP/app/reducers/interviewInfo'; 30 | 31 | const mapState = (state) => { 32 | return { 33 | user: state.auth, 34 | createInterviewAndRedirect: () => { 35 | createInterviewAndRedirect(state.auth.id); 36 | } 37 | }; 38 | }; 39 | 40 | const mapDispatch = (dispatch) => { 41 | return { 42 | }; 43 | }; 44 | 45 | export default connect(mapState, mapDispatch)(InterviewerDashboard); 46 | -------------------------------------------------------------------------------- /app/components/Jokes.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class BonesJokes extends Component { 4 | componentDidMount() { 5 | this.nextJoke() 6 | } 7 | 8 | nextJoke = () => 9 | this.setState({ 10 | joke: randomJoke(), 11 | answered: false, 12 | }) 13 | 14 | answer = () => 15 | this.setState({answered: true}) 16 | 17 | render() { 18 | if (!this.state) { return null } 19 | 20 | const {joke, answered} = this.state 21 | return ( 22 |
23 |

{joke.q}

24 | {answered &&

{joke.a}

} 25 | ~xoxo, bones 26 |
27 | ) 28 | } 29 | } 30 | 31 | function randomJoke() { 32 | return jokes[Math.floor(Math.random() * jokes.length)] 33 | } 34 | 35 | const jokes = `Q: Who won the skeleton beauty contest? 36 | A: No body 37 | Q: What do skeletons say before they begin dining? 38 | A: Bone appetit ! 39 | Q: When does a skeleton laugh? 40 | A: When something tickles his funny bone. 41 | Q: Why didn't the skeleton dance at the Halloween party? 42 | A: It had no body to dance with. 43 | Q: What type of art do skeletons like? 44 | A: Skull tures 45 | Q: What did the skeleton say when his brother told a lie? 46 | A: You can't fool me, I can see right through you. 47 | Q: What did the skeleton say while riding his Harley Davidson motorcycle? 48 | A: I'm bone to be wild! 49 | Q: Why didn't the skeleton dance at the party? 50 | A: He had no body to dance with. 51 | Q: What do you give a skeleton for valentine's day? 52 | A: Bone-bones in a heart shaped box. 53 | Q: Who was the most famous skeleton detective? 54 | A: Sherlock Bones. 55 | Q: Who was the most famous French skeleton? 56 | A: Napoleon bone-apart 57 | Q: What instrument do skeletons play? 58 | A: Trom-BONE. 59 | Q: What does a skeleton orders at a restaurant? 60 | A: Spare ribs!!! 61 | Q: When does a skeleton laugh? 62 | A: When something tickles his funny bone. 63 | Q: Why didn't the skeleton eat the cafeteria food? 64 | A: Because he didn't have the stomach for it! 65 | Q: Why couldn't the skeleton cross the road? 66 | A: He didn't have the guts. 67 | Q: Why are skeletons usually so calm ? 68 | A: Nothing gets under their skin ! 69 | Q: Why do skeletons hate winter? 70 | A: Beacuse the cold goes right through them ! 71 | Q: Why are graveyards so noisy ? 72 | A: Beacause of all the coffin ! 73 | Q: Why didn't the skeleton go to the party ? 74 | A: He had no body to go with ! 75 | Q: What happened when the skeletons rode pogo sticks ? 76 | A: They had a rattling good time ! 77 | Q: Why did the skeleton go to hospital ? 78 | A: To have his ghoul stones removed ! 79 | Q: How did the skeleton know it was going to rain ? 80 | A: He could feel it in his bones ! 81 | Q: What's a skeleton's favourite musical instrument ? 82 | A: A trom-bone ! 83 | Q: How do skeletons call their friends ? 84 | A: On the telebone ! 85 | Q: What do you call a skeleton who won't get up in the mornings ? 86 | A: Lazy bones ! 87 | Q: What do boney people use to get into their homes ? 88 | A: Skeleton keys ! 89 | Q: What do you call a skeleton who acts in Westerns ? 90 | A: Skint Eastwood ! 91 | Q: What happened to the boat that sank in the sea full of piranha fish ? 92 | A: It came back with a skeleton crew ! 93 | Q: What do you call a skeleton snake ? 94 | A: A rattler ! 95 | Q: What is a skeletons like to drink milk ? 96 | A: Milk - it's so good for the bones ! 97 | Q: Why did the skeleton stay out in the snow all night ? 98 | A: He was a numbskull ! 99 | Q: What do you call a stupid skeleton ? 100 | A: Bonehead ! 101 | Q: What happened to the skeleton who stayed by the fire too long ? 102 | A: He became bone dry ! 103 | Q: What happened to the lazy skeleton ? 104 | A: He was bone idle ! 105 | Q: Why did the skeleton pupil stay late at school ? 106 | A: He was boning up for his exams ! 107 | Q: What sort of soup do skeletons like ? 108 | A: One with plenty of body in it ! 109 | Q: Why did the skeleton run up a tree ? 110 | A: Because a dog was after his bones ! 111 | Q: What did the skeleton say to his girlfriend ? 112 | A: I love every bone in your body ! 113 | Q: Why wasn't the naughty skeleton afraid of the police ? 114 | A: Because he knew they couldn't pin anything on him ! 115 | Q: How do skeletons get their mail ? 116 | A: By bony express ! 117 | Q: Why don't skeletons play music in church ? 118 | A: They have no organs ! 119 | Q: What kind of plate does a skeleton eat off ? 120 | A: Bone china ! 121 | Q: Why do skeletons hate winter ? 122 | A: Because the wind just goes straight through them ! 123 | Q: What's a skeleton's favourite pop group ? 124 | A: Boney M ! 125 | Q: What do you do if you see a skeleton running across a road ? 126 | A: Jump out of your skin and join him ! 127 | Q: What did the old skeleton complain of ? 128 | A: Aching bones ! 129 | Q: What is a skeleton ? 130 | A: Somebody on a diet who forgot to say "when" ! 131 | Q: What happened to the skeleton that was attacked by a dog ? 132 | A: He ran off with some bones and didn't leave him with a leg to stand on ! 133 | Q: Why are skeletons so calm ? 134 | A: Because nothing gets under their skin ! 135 | Q: What do you call a skeleton that is always telling lies ? 136 | A: A boney phoney ! 137 | Q: Why didn't the skeleton want to play football ? 138 | A: Because his heart wasn't in it ! 139 | Q: What happened to the skeleton who went to a party ? 140 | A: All the others used him as a coat rack ! 141 | Q: What do you call a skeleton who presses the door bell ? 142 | A: A dead ringer ! 143 | Q: When does a skeleton laugh? 144 | A: When something tickles his funny bone. 145 | Q: How did skeletons send their letters in the old days? 146 | A: By bony express! 147 | Q: How do you make a skeleton laugh? 148 | A: Tickle his funny bone!` 149 | .split('\n') 150 | .reduce((all, row, i) => 151 | i % 2 === 0 152 | ? [...all, {q: row}] 153 | : [...all.slice(0, all.length - 1), Object.assign({a: row}, all[all.length - 1])], 154 | []) -------------------------------------------------------------------------------- /app/components/Jokes.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | 6 | import Jokes from './Jokes' 7 | 8 | xdescribe('', () => { 9 | const joke = { 10 | q: 'Why did the skeleton write tests?', 11 | a: 'To see if she did anything bone-headed.', 12 | } 13 | 14 | let root 15 | beforeEach('render the root', () => 16 | root = shallow() 17 | ) 18 | 19 | it('shows a joke', () => { 20 | root.setState({ joke, answered: false }) 21 | expect(root.find('h1')).to.have.length(1) 22 | expect(root.find('h1').text()).equal(joke.q) 23 | }) 24 | 25 | it("doesn't show the answer when state.answered=false", () => { 26 | root.setState({ joke, answered: false }) 27 | expect(root.find('h2')).to.have.length(0) 28 | }) 29 | 30 | it('shows the answer when state.answered=true', () => { 31 | root.setState({ joke, answered: true }) 32 | expect(root.find('h2')).to.have.length(1) 33 | expect(root.find('h2').text()).to.equal(joke.a) 34 | }) 35 | 36 | it('when tapped, sets state.answered=true', () => { 37 | root.setState({ joke, answered: false }) 38 | root.simulate('click') 39 | expect(root.state().answered).to.be.true 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /app/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import WhoAmI from './WhoAmI'; 3 | 4 | import TextField from 'material-ui/TextField'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | 7 | const styles = { 8 | margin: '10px' 9 | }; 10 | 11 | export class Login extends React.Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | username: '', 16 | password: '' 17 | }; 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |
{ 24 | evt.preventDefault(); 25 | this.props.login(evt.target.username.value, evt.target.password.value); 26 | } }> 27 |
28 | this.setState({username: e.target.value})} 32 | /> 33 |
34 |
35 | this.setState({password: e.target.value})} 40 | /> 41 |
42 |
43 | this.props.signup(null, this.state.username, this.state.password)} 48 | /> 49 | 55 |
56 | 57 |
58 | ); 59 | } 60 | } 61 | 62 | import { login, signup } from 'APP/app/reducers/auth'; 63 | import { connect } from 'react-redux'; 64 | 65 | export default connect(state => ({}), { login, signup })(Login); 66 | 67 | -------------------------------------------------------------------------------- /app/components/Login.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | 8 | import {Login} from './Login' 9 | 10 | xdescribe('', () => { 11 | let root 12 | beforeEach('render the root', () => 13 | root = shallow() 14 | ) 15 | 16 | it('shows a login form', () => { 17 | expect(root.find('input[name="username"]')).to.have.length(1) 18 | expect(root.find('input[name="password"]')).to.have.length(1) 19 | }) 20 | 21 | it('shows a password field', () => { 22 | const pw = root.find('input[name="password"]') 23 | expect(pw).to.have.length(1) 24 | expect(pw.at(0)).to.have.attr('type').equals('password') 25 | }) 26 | 27 | it('has a login button', () => { 28 | const submit = root.find('input[type="submit"]') 29 | expect(submit).to.have.length(1) 30 | }) 31 | 32 | xdescribe('when submitted', () => { 33 | const login = spy() 34 | const root = shallow() 35 | const submitEvent = { 36 | preventDefault: spy(), 37 | target: { 38 | username: {value: 'bones@example.com'}, 39 | password: {value: '12345'} 40 | } 41 | } 42 | 43 | beforeEach('submit', () => { 44 | login.reset() 45 | submitEvent.preventDefault.reset() 46 | root.simulate('submit', submitEvent) 47 | }) 48 | 49 | it('calls props.login with credentials', () => { 50 | expect(login).to.have.been.calledWith( 51 | submitEvent.target.username.value, 52 | submitEvent.target.password.value, 53 | ) 54 | }) 55 | 56 | it('calls preventDefault', () => { 57 | expect(submitEvent.preventDefault).to.have.been.called 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /app/components/RateProblem.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import Rating from 'react-rating'; 4 | 5 | // Required files 6 | 7 | /* ----------------- COMPONENT ------------------ */ 8 | 9 | export const RateProblem = ({ problem, handleRate, candidateRating }) => { 10 | return ( 11 |
12 |

Problem: { problem.name }

13 | 14 |
15 | ); 16 | }; 17 | 18 | /* ----------------- CONTAINER ------------------ */ 19 | 20 | // Required libraries 21 | import { connect } from 'react-redux'; 22 | 23 | // Required files 24 | import { setCandidateRating } from 'APP/app/reducers/interviewProblems'; 25 | 26 | const mapState = (state, ownProps) => { 27 | const { candidateRating } = ownProps.problem.interviewProblems; 28 | return { 29 | candidateRating 30 | }; 31 | }; 32 | 33 | const mapDispatch = (dispatch, ownProps) => { 34 | const { problem_id, interview_id } = ownProps.problem.interviewProblems; 35 | return { 36 | handleRate: rating => { 37 | setCandidateRating(interview_id, problem_id, rating); 38 | } 39 | }; 40 | }; 41 | 42 | export default connect(mapState, mapDispatch)(RateProblem); 43 | -------------------------------------------------------------------------------- /app/components/RoomSelectBtn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Faker from 'faker'; 3 | 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import ArrowRight from 'material-ui/svg-icons/hardware/keyboard-arrow-right'; 6 | 7 | const roomName = Faker.fake("{{name.prefix}}{{hacker.adjective}}{{hacker.noun}}{{random.word}}{{random.number}}"); 8 | 9 | const styles = { 10 | button: { 11 | margin: 12 12 | }, 13 | exampleImageInput: { 14 | cursor: 'pointer', 15 | position: 'absolute', 16 | top: 0, 17 | bottom: 0, 18 | right: 0, 19 | left: 0, 20 | width: '100%', 21 | opacity: 0 22 | } 23 | }; 24 | 25 | export default () => { 26 | return ( 27 |
28 | } 33 | style={styles.button} 34 | href={ `/interviewRoom/${ roomName }` } 35 | /> 36 |
37 | ); 38 | }; -------------------------------------------------------------------------------- /app/components/Signup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {signup} from '../reducers/auth'; 4 | import TextField from 'material-ui/TextField'; 5 | import RaisedButton from 'material-ui/RaisedButton'; 6 | 7 | class Signup extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | name: '', 12 | username: '', 13 | password: '' 14 | }; 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | this.setState({name: e.target.value})} 25 | /> 26 |
27 |
28 | this.setState({username: e.target.value.toLowerCase()})} 32 | 33 | /> 34 |
35 |
36 | this.setState({password: e.target.value})} 41 | 42 | /> 43 |
44 |
45 | this.props.signup(this.state.name, this.state.username, this.state.password)} 50 | /> 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default connect( 58 | state => ({}), 59 | {signup} 60 | )(Signup); 61 | -------------------------------------------------------------------------------- /app/components/WhiteboardConstants.jsx: -------------------------------------------------------------------------------- 1 | // palette colors: black, green, red, blue, purple, orange 2 | export const PALETTE = ['#000000', '#3DA264', '#BE1A1A', '#004DCF', '#5300EB', '#DB3E00']; 3 | export const DEFAULT_STROKE_SIZE = 4; 4 | export const ERASER_STROKE_SIZE = 50; 5 | export const DEFAULT_COLOR = '#000000'; 6 | export const BOARD_COLOR = '#FFFFFF'; 7 | -------------------------------------------------------------------------------- /app/components/WhiteboardContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import Konva from 'react-konva'; 4 | import Immutable from 'immutable'; 5 | import {WhiteboardToolbar} from './WhiteboardToolbar'; 6 | import {DEFAULT_STROKE_SIZE, ERASER_STROKE_SIZE, BOARD_COLOR} from './WhiteboardConstants'; 7 | 8 | // ------------------------ Whiteboard Dumb Component ------------------------ // 9 | export const Whiteboard = (props) => { 10 | let history = props.history; 11 | let show = (history.length !== 0); 12 | return ( 13 | 16 | 17 | 18 | {show && history.map((drawEvent, i) => { 19 | return ( 20 | 28 | ); 29 | })} 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | // ------------------------ Whiteboard Container ------------------------ // 37 | export class WhiteboardContainer extends React.Component { 38 | constructor (props) { 39 | super(props); 40 | this.state = { 41 | drawing: this.props.drawing, 42 | lastX: this.props.lastDraw.lastPx.x, 43 | lastY: this.props.lastDraw.lastPx.y, 44 | currentX: this.props.lastDraw.currentPx.x, 45 | currentY: this.props.lastDraw.currentPx.y, 46 | color: this.props.lastDraw.color, 47 | strokeWidth: this.props.lastDraw.strokeWidth 48 | }; 49 | 50 | this.handleMouseDown = this.handleMouseDown.bind(this); 51 | this.handleMouseUp = this.handleMouseUp.bind(this); 52 | this.handleMouseMove = this.handleMouseMove.bind(this); 53 | this.handleColorChange = this.handleColorChange.bind(this); 54 | this.handleClear = this.handleClear.bind(this); 55 | this.handleErase = this.handleErase.bind(this); 56 | } 57 | 58 | handleMouseDown (event) { 59 | this.setState({ 60 | drawing: true, 61 | currentX: event.evt.offsetX, 62 | currentY: event.evt.offsetY 63 | }); 64 | } 65 | 66 | handleColorChange (color) { 67 | // Update current mouse position 68 | this.setState({ 69 | color: color.hex, 70 | strokeWidth: DEFAULT_STROKE_SIZE 71 | }); 72 | } 73 | 74 | handleClear () { 75 | this.props.clearHistory(); 76 | } 77 | 78 | handleErase () { 79 | this.setState({ 80 | color: BOARD_COLOR, 81 | strokeWidth: ERASER_STROKE_SIZE 82 | }); 83 | } 84 | 85 | handleMouseMove (event) { 86 | if (!this.state.drawing) return; // Short circuit if mouse button isn't down 87 | // Update last and current mouse positions 88 | this.setState({ 89 | lastX: this.state.currentX, 90 | lastY: this.state.currentY, 91 | currentX: event.evt.offsetX, 92 | currentY: event.evt.offsetY 93 | }); 94 | 95 | // Send dispatch out for new coordinates (end of stroke) 96 | this.props.setCoordinates( 97 | {x: this.state.lastX, y: this.state.lastY}, 98 | {x: this.state.currentX, y: this.state.currentY}, 99 | this.state.color, 100 | this.state.strokeWidth 101 | ); 102 | } 103 | 104 | handleMouseUp () { 105 | this.setState({drawing: false}); 106 | } 107 | 108 | render () { 109 | return ( 110 |
111 | 118 | 124 |
125 | ); 126 | } 127 | } 128 | 129 | /* ----------------- CONNECT CONTAINER ------------------ */ 130 | 131 | import { setCoordinates, clearHistory } from '../reducers/whiteboard'; 132 | 133 | const mapStateToProps = (state, ownProps) => { 134 | return { 135 | lastDraw: state.interview.whiteboard.get('lastDraw').toJS(), 136 | drawingHistory: state.interview.whiteboard.get('drawingHistory').toJS(), 137 | handleClose: ownProps.handleClose 138 | }; 139 | }; 140 | const mapDispatchToProps = (dispatch) => { 141 | return { 142 | setCoordinates: (lastPx, currentPx, color, strokeWidth) => { 143 | dispatch(setCoordinates(lastPx, currentPx, color, strokeWidth)); 144 | }, 145 | clearHistory: () => { 146 | dispatch(clearHistory()); 147 | } 148 | }; 149 | }; 150 | 151 | export default connect(mapStateToProps, mapDispatchToProps)(WhiteboardContainer); 152 | -------------------------------------------------------------------------------- /app/components/WhiteboardToolbar.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import { GithubPicker } from 'react-color'; 4 | import RaisedButton from 'material-ui/RaisedButton'; 5 | import IconButton from 'material-ui/IconButton'; 6 | import Popover from 'material-ui/Popover'; 7 | import {Toolbar, ToolbarGroup, ToolbarSeparator, ToolbarTitle} from 'material-ui/Toolbar'; 8 | import Close from 'material-ui/svg-icons/navigation/close'; 9 | import Stop from 'material-ui/svg-icons/av/stop'; 10 | import {PALETTE} from './WhiteboardConstants'; 11 | 12 | /* ----------------- COMPONENT ------------------ */ 13 | // 14 | export class WhiteboardToolbar extends React.Component { 15 | // need color, strokewidth 16 | constructor (props) { 17 | super(props); 18 | this.state = { 19 | open: false 20 | }; 21 | 22 | this.handleTouchTap = this.handleTouchTap.bind(this); 23 | this.handlePickerResult = this.handlePickerResult.bind(this); 24 | this.handleRequestClose = this.handleRequestClose.bind(this); 25 | } 26 | 27 | handleTouchTap (event) { 28 | // This prevents ghost click. 29 | event.preventDefault(); 30 | 31 | this.setState({ 32 | open: true, 33 | anchorEl: event.currentTarget 34 | }); 35 | } 36 | 37 | handlePickerResult (color) { 38 | this.props.handleColorChange(color); 39 | this.handleRequestClose(); 40 | } 41 | 42 | handleRequestClose () { 43 | this.setState({ 44 | open: false 45 | }); 46 | } 47 | 48 | render () { 49 | const handleClose = this.props.handleClose; 50 | const color = this.props.color; 51 | const handleClear = this.props.handleClear; 52 | const handleErase = this.props.handleErase; 53 | const tightStyle = {padding: '0px', border: '0px', margin: '0px'}; 54 | return ( 55 | 56 | 57 | 58 | 64 | > 65 | 72 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/components/WhoAmI.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const WhoAmI = ({ user, logout }) => ( 4 |
5 | {user && user.name} 6 | 7 |
8 | ) 9 | 10 | import {logout} from 'APP/app/reducers/auth' 11 | import {connect} from 'react-redux' 12 | 13 | export default connect ( 14 | ({ auth }) => ({ user: auth }), 15 | {logout}, 16 | ) (WhoAmI) -------------------------------------------------------------------------------- /app/components/WhoAmI.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import chai, {expect} from 'chai' 3 | chai.use(require('chai-enzyme')()) 4 | import {shallow} from 'enzyme' 5 | import {spy} from 'sinon' 6 | chai.use(require('sinon-chai')) 7 | import {createStore} from 'redux' 8 | 9 | import WhoAmIContainer, {WhoAmI} from './WhoAmI' 10 | 11 | xdescribe('', () => { 12 | const user = { 13 | name: 'Dr. Bones', 14 | } 15 | const logout = spy() 16 | let root 17 | beforeEach('render the root', () => 18 | root = shallow() 19 | ) 20 | 21 | it('greets the user', () => { 22 | expect(root.text()).to.contain(user.name) 23 | }) 24 | 25 | it('has a logout button', () => { 26 | expect(root.find('button.logout')).to.have.length(1) 27 | }) 28 | 29 | it('calls props.logout when logout is tapped', () => { 30 | root.find('button.logout').simulate('click') 31 | expect(logout).to.have.been.called 32 | }) 33 | }) 34 | 35 | xdescribe("'s connection", () => { 36 | const state = { 37 | auth: {name: 'Dr. Bones'} 38 | } 39 | 40 | let root, store, dispatch 41 | beforeEach('create store and render the root', () => { 42 | store = createStore(state => state, state) 43 | dispatch = spy(store, 'dispatch') 44 | root = shallow() 45 | }) 46 | 47 | it('gets prop.user from state.auth', () => { 48 | expect(root.find(WhoAmI)).to.have.prop('user').eql(state.auth) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /app/components/interview-room/InterviewRoom.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | 4 | // Required files 5 | import AppBar from 'material-ui/AppBar'; 6 | import Drawer from 'material-ui/Drawer'; 7 | import IconButton from 'material-ui/IconButton'; 8 | import Menu from 'material-ui/svg-icons/action/settings'; 9 | // import Refresh from 'material-ui/svg-icons/navigation/refresh'; 10 | import Close from 'material-ui/svg-icons/navigation/close'; 11 | import AlertError from 'material-ui/svg-icons/alert/error'; 12 | import Gesture from 'material-ui/svg-icons/content/gesture'; 13 | import { blueGrey500 } from 'material-ui/styles/colors'; 14 | 15 | import WhiteboardContainer from '../WhiteboardContainer'; 16 | import Editor from '../Editor'; 17 | import InterviewRoomOptions from '../InterviewRoomOptions'; 18 | 19 | /* ----------------- COMPONENT ------------------ */ 20 | 21 | export class InterviewRoom extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.iconStyles = { 25 | marginRight: 24, 26 | marginTop: 10, 27 | backgroundColor: blueGrey500 28 | }; 29 | this.WBStyles = { 30 | width: "50%" 31 | }; 32 | this.state = { 33 | open: false, 34 | WBOpen: false 35 | }; 36 | this.handleOpen = this.handleOpen.bind(this); 37 | this.handleClose = this.handleClose.bind(this); 38 | this.handleWBOpen = this.handleWBOpen.bind(this); 39 | this.handleWBClose = this.handleWBClose.bind(this); 40 | } 41 | 42 | handleOpen() { 43 | this.setState({ open: !this.state.open, WBOpen: false }); 44 | } 45 | handleClose() { 46 | this.setState({ open: false }); 47 | } 48 | handleWBOpen() { 49 | this.setState({ open: false, WBOpen: !this.state.WBOpen }); 50 | } 51 | handleWBClose() { 52 | this.setState({ open: false, WBOpen: false }); 53 | } 54 | 55 | render() { 56 | return ( 57 |
58 | CodeHuddle } 60 | iconElementLeft={ } 61 | iconElementRight={ } 62 | titleStyle={{ fontFamily: 'Aldrich', textAlign: 'center' }} 63 | style={{ backgroundColor: blueGrey500 }}/> 64 | {/* Left Side Drawer */} 65 | this.setState({open})} 70 | > 71 | 72 | 73 | 74 | 75 | {/* Right Side Drawer */} 76 | 81 | 82 | 83 | {/* Page Content */} 84 |
85 | 86 |
87 |
88 | ); 89 | } 90 | } 91 | 92 | /* ----------------- CONTAINER ------------------ */ 93 | 94 | // Required libraries 95 | import { connect } from 'react-redux'; 96 | 97 | const mapState = (state) => { 98 | return { 99 | }; 100 | }; 101 | 102 | const mapDispatch = (dispatch) => { 103 | return { 104 | }; 105 | }; 106 | 107 | export default connect(mapState, mapDispatch)(InterviewRoom); 108 | -------------------------------------------------------------------------------- /app/components/splash/Home.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import React from 'react'; 3 | import RoomSelectBtn from '../RoomSelectBtn'; 4 | 5 | // Required files 6 | import RaisedButton from 'material-ui/RaisedButton'; 7 | import IconButton from 'material-ui/IconButton'; 8 | import Dialog from 'material-ui/Dialog'; 9 | import FlatButton from 'material-ui/FlatButton'; 10 | import NavigateNext from 'material-ui/svg-icons/image/navigate-next'; 11 | import Login from '../Login'; 12 | import ArrowDown from 'material-ui/svg-icons/hardware/keyboard-arrow-down'; 13 | import ArrowRight from 'material-ui/svg-icons/hardware/keyboard-arrow-right'; 14 | 15 | export default class Home extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { open: false }; 19 | this.handleOpen = this.handleOpen.bind(this); 20 | this.handleClose = this.handleClose.bind(this); 21 | } 22 | 23 | handleOpen() { 24 | this.setState({open: true}); 25 | } 26 | 27 | handleClose() { 28 | this.setState({open: false}); 29 | } 30 | render() { 31 | const actions = [ 32 | 37 | ]; 38 | return ( 39 |
40 | 48 | 49 | 89 | 90 |
91 |
92 | 93 |
94 |
95 |
96 |

Ace Code Editor

97 |

Lorem ipsum dolor sit amet, et nibh nonummy lobortis ultricies, nisl morbi vivamus amet quo, amet ullamcorper lacus maecenas, egestas bibendum elit scelerisque mollis. Tempor est risus sociosqu luctus, turpis fusce facilisi ligula, volutpat ipsum morbi in, erat mauris suspendisse eros ante. Pede purus elit velit ut, ut interdum felis interdum tristique, odio eu ipsum cras, urna est litora in. Dolor sem ipsum amet vivamus, lectus nec fusce porta, felis quis tellus ligula. Augue odio fermentum turpis dignissim, hendrerit posuere libero convallis vel.

98 |
99 |
100 |

Built-In Whiteboard

101 |

Lorem ipsum dolor sit amet, et nibh nonummy lobortis ultricies, nisl morbi vivamus amet quo, amet ullamcorper lacus maecenas, egestas bibendum elit scelerisque mollis. Tempor est risus sociosqu luctus, turpis fusce facilisi ligula, volutpat ipsum morbi in, erat mauris suspendisse eros ante. Pede purus elit velit ut, ut interdum felis interdum tristique, odio eu ipsum cras, urna est litora in. Dolor sem ipsum amet vivamus, lectus nec fusce porta, felis quis tellus ligula. Augue odio fermentum turpis dignissim, hendrerit posuere libero convallis vel.

102 |
103 |
104 |
105 | 106 |
107 |
108 | 109 |
110 |
111 | © 2017 Copyright Amrom Steinmetz, Amy Paschal, Andrew Garcia, Evan DiGiambattista, Surabhi Nigam 112 |
113 |
114 |
115 | ); 116 | 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/components/splash/Splash-Content.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | import SCHeader from './splash-content/SC-header'; 6 | import SCContent1 from './splash-content/SC-content-1.jsx'; 7 | import SCHipster from './splash-content/SC-content-2.jsx'; 8 | import SCHired from './splash-content/SC-content-3.jsx'; 9 | import SplashFooter from './Splash-Footer'; 10 | 11 | export default () => { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /app/components/splash/Splash-Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default (props) => { 4 | return ( 5 |
6 |
7 |
8 |
Footer Content
9 |

You can use rows and columns here to organize your footer content.

10 |
11 |
12 |
Links
13 | 19 |
20 |
21 | 22 |
23 |
24 | © 2017 Copyright Amrom Steinmetz, Amy Paschal, Andrew Garcia, Evan DiGiambattista, Surabhi Nigam 25 | More Links 26 |
27 |
28 |
29 | ); 30 | }; -------------------------------------------------------------------------------- /app/components/splash/Splash-Nav-1.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | 5 | return ( 6 |
7 | 13 |
14 | ); 15 | }; -------------------------------------------------------------------------------- /app/components/splash/Splash-Nav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | return ( 5 |
6 | 24 |
25 | ); 26 | }; -------------------------------------------------------------------------------- /app/components/splash/Splash.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import SplashNav1 from './Splash-Nav-1'; 5 | import SplashContent from './Splash-Content'; 6 | // import SplashFooter from './Splash-Footer'; 7 | 8 | export const Splash = () => { 9 | 10 | return ( 11 |
12 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | import { connect } from 'react-redux'; 20 | 21 | const mapStateToProps = null; 22 | const mapDispatchToProps = null; 23 | 24 | export default connect(mapStateToProps, mapDispatchToProps)(Splash); 25 | -------------------------------------------------------------------------------- /app/components/splash/splash-content/SC-content-1.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export default () => { 6 | 7 | return ( 8 |
9 |
10 |
11 |

Ace Code Editor

12 | code 13 |

Lorem ipsum dolor sit amet, et nibh nonummy lobortis ultricies, nisl morbi vivamus amet quo, amet ullamcorper lacus maecenas, egestas bibendum elit scelerisque mollis. Tempor est risus sociosqu luctus, turpis fusce facilisi ligula, volutpat ipsum morbi in, erat mauris suspendisse eros ante. Pede purus elit velit ut, ut interdum felis interdum tristique, odio eu ipsum cras, urna est litora in. Dolor sem ipsum amet vivamus, lectus nec fusce porta, felis quis tellus ligula. Augue odio fermentum turpis dignissim, hendrerit posuere libero convallis vel.

14 |
15 |
16 |

Built-In Whiteboard

17 | palette 18 |

Lorem ipsum dolor sit amet, et nibh nonummy lobortis ultricies, nisl morbi vivamus amet quo, amet ullamcorper lacus maecenas, egestas bibendum elit scelerisque mollis. Tempor est risus sociosqu luctus, turpis fusce facilisi ligula, volutpat ipsum morbi in, erat mauris suspendisse eros ante. Pede purus elit velit ut, ut interdum felis interdum tristique, odio eu ipsum cras, urna est litora in. Dolor sem ipsum amet vivamus, lectus nec fusce porta, felis quis tellus ligula. Augue odio fermentum turpis dignissim, hendrerit posuere libero convallis vel.

19 |
20 |
21 |
22 | ); 23 | }; -------------------------------------------------------------------------------- /app/components/splash/splash-content/SC-content-2.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export default () => { 6 | 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | ); 14 | }; -------------------------------------------------------------------------------- /app/components/splash/splash-content/SC-content-3.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | 5 | export default () => { 6 | 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | ); 14 | }; -------------------------------------------------------------------------------- /app/components/splash/splash-content/SC-header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from 'material-ui/Dialog/Dialog'; 3 | import FlatButton from 'material-ui/FlatButton/FlatButton'; 4 | import RaisedButton from 'material-ui/RaisedButton/RaisedButton'; 5 | 6 | export default class SCHeader extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | open: false 11 | }; 12 | 13 | this.handleOpen = this.handleOpen.bind(this); 14 | this.handleClose = this.handleClose.bind(this); 15 | } 16 | 17 | handleOpen() { 18 | this.setState({ open: true }); 19 | } 20 | handleClose() { 21 | this.setState({ open: false }); 22 | } 23 | 24 | 25 | render() { 26 | const actions = [ 27 | , 32 | 38 | ]; 39 | return ( 40 |
41 |
42 |
    43 |
  • 44 |
  • Code Huddle

  • 45 |
46 |
47 |

Technical Interviews Done Right

48 |
49 |
    50 |
  • 51 |
52 | 57 | Only actions can close this dialog. 58 | 59 |
60 | keyboard_arrow_down 61 |
62 | ); 63 | } 64 | } -------------------------------------------------------------------------------- /app/main.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Required libraries 4 | import React from 'react'; 5 | import { Router, Route, IndexRedirect, browserHistory } from 'react-router'; 6 | import { render } from 'react-dom'; 7 | import { Provider } from 'react-redux'; 8 | 9 | // Required files 10 | import store from './store'; 11 | import Routes from './Routes'; 12 | 13 | // For Material-UI: Provides `onTouchTap()` event; Much like an `onClick()` for touch devices. 14 | import injectTapEventPlugin from 'react-tap-event-plugin'; 15 | injectTapEventPlugin(); 16 | 17 | // Material Theme Provider. Wraps everything in `render()` method below 18 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; 19 | import getMuiTheme from 'material-ui/styles/getMuiTheme'; 20 | import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme'; 21 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; 22 | 23 | 24 | render( 25 | 26 | 27 | 28 | 29 | , 30 | document.getElementById('main') 31 | ); 32 | -------------------------------------------------------------------------------- /app/reducers/allInterviews.jsx: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | import Immutable from 'immutable'; 4 | import axios from 'axios'; 5 | 6 | /* ----------------- ACTIONS ------------------ */ 7 | const SET_INTERVIEWS = 'SET_INTERVIEWS'; 8 | 9 | /* ------------ ACTION CREATORS ------------------ */ 10 | export const setInterviews = interviews => ({ 11 | type: SET_INTERVIEWS, 12 | interviews 13 | }); 14 | 15 | /* ------------ REDUCER ------------------ */ 16 | const initialInterviewsData = Immutable.fromJS( 17 | [] 18 | ); 19 | 20 | export default function reducer (interviewsData = initialInterviewsData, action) { 21 | let newInterviewsData; 22 | switch (action.type) { 23 | case SET_INTERVIEWS: 24 | newInterviewsData = Immutable.fromJS(action.interviews); 25 | break; 26 | 27 | default: return interviewsData; 28 | 29 | } 30 | 31 | return newInterviewsData; 32 | } 33 | 34 | /* ------------ DISPATCHERS ------------------ */ 35 | 36 | export const fetchAllInterviews = userID => { 37 | return dispatch => { 38 | return axios.get(`/api/interviews`) 39 | .then(response => { 40 | return dispatch(setInterviews(response.data)); 41 | }) 42 | .catch(console.error); 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /app/reducers/auth.jsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {browserHistory} from 'react-router'; 3 | 4 | const reducer = (state = null, action) => { 5 | switch (action.type) { 6 | case AUTHENTICATED: 7 | console.log(action.user); 8 | return action.user; 9 | } 10 | return state; 11 | }; 12 | 13 | const AUTHENTICATED = 'AUTHENTICATED'; 14 | export const authenticated = user => ({ 15 | type: AUTHENTICATED, user 16 | }); 17 | 18 | export const login = (username, password) => 19 | dispatch => 20 | // we'll need to change this to '/api/auth/local/login' if we implement OAuth 21 | axios.post('/api/auth/login', 22 | {username, password}) 23 | .then(() => { 24 | dispatch(whoami()); 25 | browserHistory.push('/interviewerDashboard'); 26 | }) 27 | .catch(() => { 28 | dispatch(whoami()); 29 | console.log('login failure'); 30 | }); 31 | 32 | export const signup = (name, email, password) => 33 | dispatch => { 34 | return axios.post('/api/users', {name, email, password}) 35 | .then(() => dispatch(login(email, password))) 36 | .catch(() => dispatch(whoami())); 37 | } 38 | 39 | 40 | export const logout = () => 41 | dispatch => 42 | axios.post('/api/auth/logout') 43 | .then(() => dispatch(whoami())) 44 | .catch(() => dispatch(whoami())); 45 | 46 | export const whoami = (arg) => { 47 | return dispatch => 48 | axios.get('/api/auth/whoami') 49 | .then(response => { 50 | const user = response.data; 51 | dispatch(authenticated(user)); 52 | }) 53 | // .catch(failed => { 54 | // console.log('fail', arg, failed); 55 | // dispatch(authenticated(null)) 56 | // }); 57 | }; 58 | 59 | export default reducer; 60 | -------------------------------------------------------------------------------- /app/reducers/editor.jsx: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | import Immutable from 'immutable'; 4 | import { socket } from 'APP/app/sockets'; 5 | 6 | /* ----------------- ACTIONS ------------------ */ 7 | const SET_TEXT = 'SET_TEXT'; 8 | const SET_OPTIONS = 'SET_OPTIONS'; 9 | const SET_RANGE = 'SET_RANGE'; 10 | const SET_RANGE_HISTORY = 'SET_RANGE_HISTORY'; 11 | 12 | /* ------------ ACTION CREATORS ------------------ */ 13 | export const setText = text => ({ 14 | type: SET_TEXT, 15 | meta: { 16 | remote: true 17 | }, 18 | text }); 19 | 20 | export const setOptions = options => ({ 21 | type: SET_OPTIONS, 22 | meta: { 23 | remote: true 24 | }, 25 | options 26 | }); 27 | 28 | export const setRange = range => ({ 29 | type: SET_RANGE, 30 | id: socket.id, 31 | meta: { 32 | remote: true 33 | }, 34 | range 35 | }); 36 | 37 | /* ------------ REDUCER ------------------ */ 38 | 39 | const defaultRange = Immutable.fromJS( 40 | { 41 | default: { 42 | start: { 43 | column: 0, 44 | row: 0 45 | }, 46 | end: { 47 | column: 0, 48 | row: 0 49 | } 50 | } 51 | } 52 | ); 53 | 54 | const initialEditorData = Immutable.fromJS( 55 | { 56 | text: `const CodeHuddle = 'built with <3';`, 57 | options: { 58 | linting: true, 59 | showGutter: true, 60 | textSize: false, 61 | theme: false 62 | }, 63 | ranges: defaultRange 64 | } 65 | ); 66 | 67 | export default function reducer (editorData = initialEditorData, action) { 68 | let newEditorData = editorData; 69 | const newRange = {}; 70 | 71 | switch (action.type) { 72 | case SET_TEXT: 73 | newEditorData = newEditorData.setIn(['text'], action.text); 74 | break; 75 | 76 | case SET_OPTIONS: 77 | newEditorData = newEditorData.mergeIn(['options'], action.options); 78 | break; 79 | 80 | case SET_RANGE: 81 | newRange[action.id] = action.range; 82 | newEditorData = newEditorData.mergeIn(['ranges'], newRange); 83 | break; 84 | 85 | case SET_RANGE_HISTORY: 86 | newEditorData = newEditorData.setIn(['ranges'], Immutable.fromJS(action.ranges)); 87 | break; 88 | 89 | default: return editorData; 90 | 91 | } 92 | 93 | return newEditorData; 94 | } 95 | 96 | /* ------------ DISPATCHERS ------------------ */ 97 | 98 | export const parseEvt = (checked, name) => { 99 | const status = {}; 100 | status[name] = checked; 101 | return status; 102 | }; 103 | -------------------------------------------------------------------------------- /app/reducers/index.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import { combineReducers } from 'redux'; 3 | 4 | // Requried files 5 | import editor from './editor'; 6 | import whiteboard from './whiteboard'; 7 | import interviewPlanning from './interviewPlanningInfo'; 8 | import interviewProblems from './interviewProblems'; 9 | import allInterviews from './allInterviews'; 10 | import interviewInfo from './interviewInfo'; 11 | 12 | const rootReducer = combineReducers({ 13 | auth: require('./auth').default, 14 | interviewPlanning, 15 | interview: combineReducers({ 16 | editor, 17 | whiteboard, 18 | interviewProblems, 19 | interviewInfo 20 | }), 21 | allInterviews 22 | }); 23 | 24 | export default rootReducer; 25 | -------------------------------------------------------------------------------- /app/reducers/interviewInfo.jsx: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | import Immutable from 'immutable'; 4 | import axios from 'axios'; 5 | import { browserHistory } from 'react-router'; 6 | 7 | /* ----------------- ACTIONS ------------------ */ 8 | const SET_INTERVIEW = 'SET_INTERVIEW'; 9 | 10 | /* ------------ ACTION CREATORS ------------------ */ 11 | export const setInterview = interview => ({ 12 | type: SET_INTERVIEW, 13 | interview 14 | }); 15 | 16 | /* ------------ REDUCER ------------------ */ 17 | const initialInterviewData = Immutable.fromJS( 18 | {} 19 | ); 20 | 21 | export default function reducer (interviewData = initialInterviewData, action) { 22 | let newInterviewData; 23 | switch (action.type) { 24 | case SET_INTERVIEW: 25 | newInterviewData = Immutable.fromJS(action.interview); 26 | break; 27 | 28 | default: return interviewData; 29 | 30 | } 31 | 32 | return newInterviewData; 33 | } 34 | 35 | /* ------------ DISPATCHERS ------------------ */ 36 | 37 | export const fetchInterview = interviewID => { 38 | return dispatch => { 39 | return axios.get(`/api/interviews/${interviewID}`) 40 | .then(response => { 41 | return dispatch(setInterview(response.data)); 42 | }) 43 | .catch(console.error); 44 | }; 45 | }; 46 | 47 | export const createInterviewAndRedirect = (userID) => { 48 | axios.post(`/api/interviews`, { interviewer_id: userID }) 49 | .then(response => response.data) 50 | .then(data => browserHistory.push(`/interviewPlanning/${data.id}`)) 51 | .catch(console.error); 52 | } 53 | -------------------------------------------------------------------------------- /app/reducers/interviewPlanningInfo.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import Immutable from 'immutable'; 3 | import axios from 'axios'; 4 | import {hashHistory} from 'react-router'; 5 | 6 | /* ----------------- ACTIONS ------------------ */ 7 | const RECEIVE_INTERVIEWS = 'RECEIVE_INTERVIEWS'; 8 | const LOAD_PROBLEMS = 'LOAD_PROBLEMS'; 9 | 10 | 11 | /* ------------ ACTION CREATORS ------------------ */ 12 | 13 | /** Need few routes 14 | 1. Interviews by userId (interviewers ID) and status 15 | 2. adding a new interview (should create a new user for the candidate? and then add ) 16 | **/ 17 | 18 | export const addInterview = (data) => { 19 | return dispatch => { 20 | return axios.post('/api/interviews/', data) 21 | .then(res => { 22 | console.log("adding interview: ", res); 23 | }); 24 | }; 25 | }; 26 | 27 | export const addProblem = (problemId, interviewId) => { 28 | return dispatch => { 29 | return axios.put('/api/interviews/', data) 30 | .then(res => { 31 | console.log("adding interview: ", res); 32 | }); 33 | }; 34 | }; 35 | 36 | export const loadProblems = problems => ({ 37 | type: LOAD_PROBLEMS, 38 | problems 39 | }); 40 | 41 | export const receiveProblems = (organization) => { 42 | return dispatch => { 43 | return axios.get(`/api/organizations/${organization}/problems`) 44 | .then(function (res) { 45 | return res.data; 46 | }) 47 | .then(function (problems) { 48 | const action = loadProblems(problems); 49 | dispatch(action); 50 | }) 51 | .catch(function (err) { 52 | console.error(err); 53 | }); 54 | }; 55 | 56 | }; 57 | 58 | 59 | /* ------------ REDUCER ------------------ */ 60 | 61 | const initialInterviewPlanningState = { 62 | problems: [] 63 | }; 64 | 65 | export default function reducer (state = initialInterviewPlanningState, action) { 66 | 67 | const newState = Object.assign({}, state); 68 | switch (action.type) { 69 | case LOAD_PROBLEMS: 70 | newState.problems = action.problems; 71 | return newState; 72 | 73 | default: return newState; 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /app/reducers/interviewProblems.jsx: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | import Immutable from 'immutable'; 4 | import axios from 'axios'; 5 | 6 | /* ----------------- ACTIONS ------------------ */ 7 | const SET_PROBLEMS = 'SET_PROBLEMS'; 8 | 9 | /* ------------ ACTION CREATORS ------------------ */ 10 | export const setProblems = problems => ({ 11 | type: SET_PROBLEMS, 12 | problems 13 | }); 14 | 15 | /* ------------ REDUCER ------------------ */ 16 | const initialProblemData = Immutable.fromJS( 17 | [] 18 | ); 19 | 20 | export default function reducer (problemData = initialProblemData, action) { 21 | let newProblemData; 22 | switch (action.type) { 23 | case SET_PROBLEMS: 24 | newProblemData = Immutable.fromJS(action.problems); 25 | break; 26 | 27 | default: return problemData; 28 | 29 | } 30 | 31 | return newProblemData; 32 | } 33 | 34 | /* ------------ DISPATCHERS ------------------ */ 35 | 36 | export const fetchProblems = interviewID => { 37 | return dispatch => { 38 | return axios.get(`/api/interviews/${interviewID}/problems`) 39 | .then(response => { 40 | console.log("inside fetchProblems: ", response.data); 41 | return dispatch(setProblems(response.data)); 42 | }) 43 | .catch(console.error); 44 | }; 45 | }; 46 | 47 | export const setCandidateRating = (interviewID, problemID, rating) => { 48 | axios.put(`/api/interviews/${interviewID}/problems/${problemID}`, { 49 | candidateRating: +rating 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /app/reducers/whiteboard.jsx: -------------------------------------------------------------------------------- 1 | // Required libraries 2 | import Immutable from 'immutable'; 3 | import {DEFAULT_COLOR, DEFAULT_STROKE_SIZE} from '../components/WhiteboardConstants'; 4 | 5 | /* ----------------- ACTIONS ------------------ */ 6 | const REQUEST_HISTORY = 'REQUEST_HISTORY'; 7 | const CLEAR_HISTORY = 'CLEAR_HISTORY'; 8 | const INIT_CANVAS = 'INIT_CANVAS'; 9 | const SET_COORDINATES = 'SET_COORDINATES'; 10 | 11 | /* ------------ ACTION CREATORS ------------------ */ 12 | 13 | export const requestHistory = () => { 14 | return { 15 | type: REQUEST_HISTORY 16 | }; 17 | }; 18 | 19 | export const clearHistory = () => { 20 | return { 21 | type: CLEAR_HISTORY, 22 | meta: { 23 | remote: true 24 | } 25 | }; 26 | }; 27 | 28 | export const initCanvas = (ctx) => { 29 | return { 30 | type: INIT_CANVAS, 31 | ctx 32 | }; 33 | }; 34 | 35 | export const setCoordinates = (lastPx, currentPx, color, strokeWidth) => { 36 | return { 37 | type: SET_COORDINATES, 38 | lastDraw: {lastPx, currentPx, color, strokeWidth}, 39 | meta: { 40 | remote: true 41 | } 42 | }; 43 | }; 44 | 45 | /* ------------ REDUCER ------------------ */ 46 | const initialWhiteboardData = Immutable.fromJS({ 47 | lastDraw: { 48 | lastPx: { x: null, y: null }, 49 | currentPx: { x: null, y: null }, 50 | color: DEFAULT_COLOR, 51 | strokeWidth: DEFAULT_STROKE_SIZE 52 | }, 53 | drawingHistory: [] 54 | }); 55 | 56 | export default function reducer (whiteboardData = initialWhiteboardData, action) { 57 | switch (action.type) { 58 | case SET_COORDINATES: 59 | const lastDrawImm = Immutable.fromJS(action.lastDraw); 60 | const newWhiteboardData = whiteboardData.setIn(['lastDraw'], lastDrawImm); 61 | const history = newWhiteboardData.getIn(['drawingHistory']); 62 | const newHistory = history.push(lastDrawImm); 63 | return newWhiteboardData.setIn(['drawingHistory'], newHistory); 64 | 65 | case REQUEST_HISTORY: 66 | return whiteboardData.setIn(['drawingHistory'], Immutable.fromJS(action.drawingHistory)); 67 | 68 | case CLEAR_HISTORY: 69 | return whiteboardData.setIn(['drawingHistory'], Immutable.fromJS([])); 70 | 71 | default: return whiteboardData; 72 | } 73 | } 74 | 75 | /* ------------ DISPATCHERS ------------------ */ 76 | -------------------------------------------------------------------------------- /app/sockets.js: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client'; 2 | export const socket = io(window.location.origin); 3 | 4 | socket.on('connect', () => { 5 | console.log('Client connected:', socket.id); 6 | }); 7 | 8 | // Sockets Middleware 9 | export const socketsEmit = (socket, channelName) => store => { 10 | socket.on(channelName, store.dispatch); // When action is received, disptach to store 11 | 12 | return next => action => { 13 | if (action.meta && action.meta.remote) { 14 | socket.emit(channelName, action); // If action has meta.remote = true, this emit to server; 15 | } 16 | 17 | return next(action); 18 | }; 19 | }; 20 | 21 | export const socketsJoinRoom = roomName => { 22 | socket.emit('wantToJoinRoom', roomName); 23 | }; 24 | -------------------------------------------------------------------------------- /app/store.jsx: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | 6 | // Required files 7 | import rootReducer from './reducers'; 8 | import { whoami } from './reducers/auth'; 9 | import { socket, socketsEmit } from 'APP/app/sockets'; 10 | 11 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 12 | 13 | const store = createStore( 14 | rootReducer, 15 | composeEnhancers(applyMiddleware( 16 | thunkMiddleware, 17 | socketsEmit(socket, 'clientStoreAction') 18 | )) 19 | ); 20 | 21 | export default store; 22 | 23 | // Set the auth info at start 24 | store.dispatch(whoami('store')); 25 | -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | 2 | export const parseToMarker = (rangeObj) => { 3 | return { 4 | startRow: rangeObj.start.row, 5 | startCol: rangeObj.start.column, 6 | endRow: rangeObj.end.row, 7 | endCol: rangeObj.end.column, 8 | className: 'editor-highlight', 9 | type: 'background', 10 | inFront: false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/build-branch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Paths to add to the deployment branch. 4 | # 5 | # These paths will be added with git add -f, to include build artifacts 6 | # we normally ignore in the branch we push to heroku. 7 | build_paths="public" 8 | 9 | # colors 10 | red='\033[0;31m' 11 | blue='\033[0;34m' 12 | off='\033[0m' 13 | 14 | echoed() { 15 | echo "${blue}${*}${off}" 16 | $* 17 | } 18 | 19 | if [[ $(git status --porcelain 2> /dev/null | grep -v '$\?\?' | tail -n1) != "" ]]; then 20 | echo "${red}Uncommitted changes would be lost. Commit or stash these changes:${off}" 21 | git status 22 | exit 1 23 | fi 24 | 25 | # Our branch name is build/commit-sha-hash 26 | version="$(git log -n1 --pretty=format:%H)" 27 | branch_name="build/${version}" 28 | 29 | 30 | function create_build_branch() { 31 | git branch "${branch_name}" 32 | git checkout "${branch_name}" 33 | return 0 34 | } 35 | 36 | function commit_build_artifacts() { 37 | # Add our build paths. -f means "even if it's in .gitignore'". 38 | git add -f "${build_paths}" 39 | 40 | # Commit the build artifacts on the branch. 41 | git commit -a -m "Built ${version} on $(date)." 42 | 43 | # Always succeed. 44 | return 0 45 | } 46 | 47 | # We expect to be sourced by some file that defines a deploy 48 | # function. If deploy() isn't defined, define a stub function. 49 | if [[ -z $(type -t deploy) ]]; then 50 | function deploy() { 51 | echo '(No deployment step defined.)' 52 | return 0 53 | } 54 | fi 55 | 56 | ( 57 | create_build_branch && 58 | echoed yarn && 59 | echoed npm run build && 60 | commit_build_artifacts && 61 | deploy 62 | 63 | # Regardless of whether we succeeded or failed, go back to 64 | # the previous branch. 65 | git checkout -- 66 | ) 67 | -------------------------------------------------------------------------------- /bin/deploy-heroku.sh: -------------------------------------------------------------------------------- 1 | # By default, we git push our build branch to heroku master. 2 | # You can specify DEPLOY_REMOTE and DEPLOY_BRANCH to configure 3 | # this. 4 | deploy_remote="${DEPLOY_REMOTE:-heroku}" 5 | deploy_branch="${DEPLOY_BRANCH:-master}" 6 | 7 | deploy() { 8 | git push -f "$deploy_remote" "$branch_name:$deploy_branch" 9 | git checkout "develop" 10 | git branch -D "$branch_name" 11 | } 12 | 13 | . "$(dirname $0)/build-branch.sh" 14 | -------------------------------------------------------------------------------- /bin/mkapplink.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const chalk = require('chalk') 4 | const fs = require('fs') 5 | const {resolve} = require('path') 6 | 7 | const appLink = resolve(__dirname, '..', 'node_modules', 'APP') 8 | 9 | const symlinkError = error => 10 | `******************************************************************* 11 | ${appLink} must point to '..' 12 | 13 | This symlink lets you require('APP/some/path') rather than 14 | ../../../some/path 15 | 16 | I tried to create it, but got this error: 17 | ${error.message} 18 | 19 | You might try this: 20 | 21 | rm ${appLink} 22 | 23 | Then run me again. 24 | 25 | ~ xoxo, bones 26 | ********************************************************************` 27 | 28 | function makeAppSymlink() { 29 | console.log(`Linking '${appLink}' to '..'`) 30 | try { 31 | try { fs.unlinkSync(appLink) } catch(swallowed) { } 32 | fs.symlinkSync('..', appLink, 'dir') 33 | } catch (error) { 34 | console.error(chalk.red(symlinkError(error))) 35 | process.exit(1) 36 | } 37 | console.log(`Ok, created ${appLink}`) 38 | } 39 | 40 | function ensureAppSymlink() { 41 | try { 42 | const currently = fs.readlinkSync(appLink) 43 | if (currently !== '..') { 44 | throw new Error(`${appLink} is pointing to '${currently}' rather than '..'`) 45 | } 46 | } catch (error) { 47 | makeAppSymlink() 48 | } 49 | } 50 | 51 | if (module === require.main) { 52 | ensureAppSymlink() 53 | } -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | mkapplink.js -------------------------------------------------------------------------------- /codehuddle.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodehuddleFSA/codehuddle/78c46b30b10c4f81cd8b3975b489f7c635610157/codehuddle.sketch -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const debug = require('debug')('sql') 3 | const chalk = require('chalk') 4 | const Sequelize = require('sequelize') 5 | const app = require('APP') 6 | 7 | const name = (process.env.DATABASE_NAME || app.name) + 8 | (app.isTesting ? '_test' : '') 9 | 10 | const url = process.env.DATABASE_URL || `postgres://localhost:5432/${name}` 11 | 12 | console.log(chalk.yellow(`Opening database connection to ${url}`)); 13 | 14 | // create the database instance 15 | const db = module.exports = new Sequelize(url, { 16 | logging: debug, // export DEBUG=sql in the environment to get SQL queries 17 | define: { 18 | underscored: true, // use snake_case rather than camelCase column names 19 | freezeTableName: true, // don't change table names from the one specified 20 | timestamps: true, // automatically include timestamp columns 21 | } 22 | }) 23 | 24 | // pull in our models 25 | require('./models') 26 | 27 | // sync the db, creating it if necessary 28 | function sync(force=app.isTesting, retries=0, maxRetries=5) { 29 | return db.sync({force}) 30 | .then(ok => console.log(`Synced models to db ${url}`)) 31 | .catch(fail => { 32 | // Don't do this auto-create nonsense in prod, or 33 | // if we've retried too many times. 34 | if (app.isProduction || retries > maxRetries) { 35 | console.error(chalk.red(`********** database error ***********`)) 36 | console.error(chalk.red(` Couldn't connect to ${url}`)) 37 | console.error() 38 | console.error(chalk.red(fail)) 39 | console.error(chalk.red(`*************************************`)) 40 | return 41 | } 42 | // Otherwise, do this autocreate nonsense 43 | console.log(`${retries ? `[retry ${retries}]` : ''} Creating database ${name}...`) 44 | return new Promise((resolve, reject) => 45 | require('child_process').exec(`createdb "${name}"`, resolve) 46 | ).then(() => sync(true, retries + 1)) 47 | }) 48 | } 49 | 50 | db.didSync = sync() -------------------------------------------------------------------------------- /db/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Require our models. Running each module registers the model into sequelize 4 | // so any other part of the application could call sequelize.model('User') 5 | // to get access to the User model. 6 | 7 | const User = require('./user'); 8 | const Interview = require('./interview'); 9 | const Organization = require('./organization'); 10 | const Problem = require('./problem'); 11 | const Solution = require('./solution'); 12 | const InterviewProblem = require('./interviewProblem'); 13 | 14 | Interview.belongsTo(User, {as: 'interviewer'}); 15 | User.belongsTo(Organization); 16 | User.hasMany(Problem, {foreignKey: 'author_id'}); 17 | User.hasMany(Interview, {foreignKey: 'interviewer_id'}); 18 | Problem.belongsTo(Organization); 19 | Problem.belongsTo(User, {as: 'author'}); 20 | Problem.belongsToMany(Interview, {through: InterviewProblem}); 21 | Problem.hasMany(Solution); 22 | Interview.belongsToMany(Problem, {through: InterviewProblem}); 23 | Organization.hasMany(Problem); 24 | Organization.hasMany(User); 25 | 26 | module.exports = { 27 | User, 28 | Interview, 29 | Organization, 30 | Problem, 31 | InterviewProblem 32 | }; 33 | -------------------------------------------------------------------------------- /db/models/interview.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Sequelize = require('sequelize'); 4 | const db = require('APP/db'); 5 | const shortid = require('shortid'); 6 | 7 | const Interview = db.define('interviews', { 8 | date: { 9 | type: Sequelize.DATE 10 | }, 11 | authToken: Sequelize.STRING, 12 | candidateName: Sequelize.STRING, 13 | candidateEmail: { 14 | type: Sequelize.STRING, 15 | validate: { 16 | isEmail: true 17 | } 18 | }, 19 | duration: Sequelize.INTEGER, 20 | position: Sequelize.STRING, 21 | status: { 22 | type: Sequelize.ENUM('planned', 'done'), 23 | defaultValue: 'planned' 24 | }, 25 | candidateOverallRating: { 26 | type: Sequelize.INTEGER, 27 | validate: { 28 | min: 0, 29 | max: 5 30 | } 31 | }, 32 | comments: Sequelize.TEXT 33 | }, { 34 | hooks: { 35 | beforeCreate: generateToken 36 | } 37 | }); 38 | 39 | function generateToken(interview) { 40 | interview.authToken = shortid.generate(); 41 | } 42 | 43 | module.exports = Interview; 44 | -------------------------------------------------------------------------------- /db/models/interviewProblem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Sequelize = require('sequelize'); 4 | const db = require('APP/db'); 5 | 6 | const InterviewProblem = db.define('interviewProblems', { 7 | interviewerRating: { 8 | type: Sequelize.INTEGER, 9 | validate: { 10 | min: 0, 11 | max: 5 12 | } 13 | }, 14 | candidateRating: { 15 | type: Sequelize.INTEGER, 16 | validate: { 17 | min: 0, 18 | max: 5 19 | } 20 | }, 21 | status: { 22 | type: Sequelize.ENUM('planned', 'used', 'not used'), 23 | defaultValue: 'planned' 24 | } 25 | }); 26 | 27 | module.exports = InterviewProblem; 28 | -------------------------------------------------------------------------------- /db/models/oauth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('oauth') 4 | const Sequelize = require('sequelize') 5 | const db = require('APP/db') 6 | 7 | const OAuth = db.define('oauths', { 8 | uid: Sequelize.STRING, 9 | provider: Sequelize.STRING, 10 | 11 | // OAuth v2 fields 12 | accessToken: Sequelize.STRING, 13 | refreshToken: Sequelize.STRING, 14 | 15 | // OAuth v1 fields 16 | token: Sequelize.STRING, 17 | tokenSecret: Sequelize.STRING, 18 | 19 | // The whole profile as JSON 20 | profileJson: Sequelize.JSON, 21 | }, { 22 | indexes: [{fields: ['uid'], unique: true,}], 23 | }) 24 | 25 | OAuth.V2 = (accessToken, refreshToken, profile, done) => 26 | this.findOrCreate({ 27 | where: { 28 | provider: profile.provider, 29 | uid: profile.id, 30 | }}) 31 | .then(oauth => { 32 | debug('provider:%s will log in user:{name=%s uid=%s}', 33 | profile.provider, 34 | profile.displayName, 35 | token.uid) 36 | oauth.profileJson = profile 37 | return db.Promise.props({ 38 | oauth, 39 | user: token.getUser(), 40 | _saveProfile: oauth.save(), 41 | }) 42 | }) 43 | .then(({ oauth, user }) => user || 44 | User.create({ 45 | name: profile.displayName, 46 | }).then(user => db.Promise.props({ 47 | user, 48 | _setOauthUser: oauth.setUser(user) 49 | })) 50 | ) 51 | .then(({ user }) => done(null, user)) 52 | .catch(done) 53 | 54 | 55 | OAuth.setupStrategy = 56 | ({ 57 | provider, 58 | strategy, 59 | config, 60 | oauth=OAuth.V2, 61 | passport 62 | }) => { 63 | const undefinedKeys = Object.keys(config) 64 | .map(k => config[k]) 65 | .filter(value => typeof value === 'undefined') 66 | if (undefinedKeys.length) { 67 | undefinedKeys.forEach(key => 68 | debug('provider:%s: needs environment var %s', provider, key)) 69 | debug('provider:%s will not initialize', provider) 70 | return 71 | } 72 | 73 | debug('initializing provider:%s', provider) 74 | passport.use(new strategy(config, oauth)) 75 | } 76 | 77 | module.exports = OAuth -------------------------------------------------------------------------------- /db/models/organization.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Sequelize = require('sequelize'); 4 | const db = require('APP/db'); 5 | const slugify = require('APP/utils').slugify; 6 | 7 | const Organization = db.define('organizations', { 8 | name: { 9 | type: Sequelize.STRING, 10 | primaryKey: true 11 | }, 12 | slug: Sequelize.STRING 13 | }, { 14 | hooks: { 15 | beforeCreate: slugify 16 | } 17 | }); 18 | 19 | module.exports = Organization; 20 | -------------------------------------------------------------------------------- /db/models/problem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Sequelize = require('sequelize'); 4 | const db = require('APP/db'); 5 | 6 | const Problem = db.define('problems', { 7 | name: { 8 | type: Sequelize.STRING, 9 | allowNull: false 10 | }, 11 | description: Sequelize.TEXT, 12 | difficulty: Sequelize.ENUM('easy', 'medium', 'hard') 13 | }); 14 | 15 | module.exports = Problem; 16 | -------------------------------------------------------------------------------- /db/models/solution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Sequelize = require('sequelize'); 4 | const db = require('APP/db'); 5 | 6 | const Solution = db.define('solutions', { 7 | code: { 8 | type: Sequelize.TEXT, 9 | allowNull: false 10 | }, 11 | bigO: Sequelize.STRING 12 | }); 13 | 14 | module.exports = Solution; 15 | -------------------------------------------------------------------------------- /db/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const bcrypt = require('bcryptjs') 4 | const Sequelize = require('sequelize') 5 | const db = require('APP/db') 6 | 7 | const User = db.define('users', { 8 | name: Sequelize.STRING, 9 | email: { 10 | type: Sequelize.STRING, 11 | validate: { 12 | isEmail: true, 13 | notEmpty: true, 14 | } 15 | }, 16 | 17 | // We support oauth, so users may or may not have passwords. 18 | password_digest: Sequelize.STRING, 19 | password: Sequelize.VIRTUAL, 20 | isAdmin: Sequelize.BOOLEAN 21 | }, { 22 | indexes: [{fields: ['email'], unique: true,}], 23 | hooks: { 24 | beforeCreate: setEmailAndPassword, 25 | beforeUpdate: setEmailAndPassword, 26 | }, 27 | instanceMethods: { 28 | authenticate(plaintext) { 29 | return new Promise((resolve, reject) => 30 | bcrypt.compare(plaintext, this.password_digest, 31 | (err, result) => 32 | err ? reject(err) : resolve(result)) 33 | ) 34 | } 35 | } 36 | }) 37 | 38 | function setEmailAndPassword(user) { 39 | user.email = user.email && user.email.toLowerCase() 40 | if (!user.password) return Promise.resolve(user) 41 | 42 | return new Promise((resolve, reject) => 43 | bcrypt.hash(user.get('password'), 10, (err, hash) => { 44 | if (err) reject(err) 45 | user.set('password_digest', hash) 46 | resolve(user) 47 | }) 48 | ) 49 | } 50 | 51 | module.exports = User -------------------------------------------------------------------------------- /db/models/user.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const User = require('./user') 5 | const {expect} = require('chai') 6 | 7 | describe('User', () => { 8 | before('wait for the db', () => db.didSync) 9 | 10 | describe('authenticate(plaintext: String) ~> Boolean', () => { 11 | it('resolves true if the password matches', () => 12 | User.create({ password: 'ok' }) 13 | .then(user => user.authenticate('ok')) 14 | .then(result => expect(result).to.be.true)) 15 | 16 | it("resolves false if the password doesn't match", () => 17 | User.create({ password: 'ok' }) 18 | .then(user => user.authenticate('not ok')) 19 | .then(result => expect(result).to.be.false)) 20 | }) 21 | }) -------------------------------------------------------------------------------- /db/seed.js: -------------------------------------------------------------------------------- 1 | const db = require('APP/db'); 2 | const faker = require('faker'); 3 | 4 | const organizations = Array(...Array(25)).map(e => ({ 5 | name: faker.company.companyName() 6 | })); 7 | 8 | const bigOs = ['O(1)', 'O(logN)', 'O(N * logN)', 'O(N)', 'O(N^2)', 'O(N!)']; 9 | 10 | const rand = (min, max) => { 11 | if (!max) { 12 | max = min; 13 | min = 0; 14 | } 15 | min = min || 0; 16 | return Math.round(Math.random() * (max - min)) + min; 17 | }; 18 | 19 | const uniqueRand = max => { 20 | const uniqueArr = Array(...Array(max)).map((_, i) => i + 1); 21 | return () => { 22 | if (max === 0) return null; 23 | const num = rand(max - 1); 24 | const uniqueVal = uniqueArr[num]; 25 | uniqueArr[num] = uniqueArr[max - 1]; 26 | max--; 27 | return uniqueVal; 28 | }; 29 | }; 30 | 31 | const seedOrganizations = () => db.Promise.map(organizations, organization => db.model('organizations').create(organization)); 32 | 33 | const seedAdminUsers = () => db.Promise.map(Array(...Array(10)).map(_ => ({ 34 | name: faker.name.findName(), 35 | email: faker.internet.email(), 36 | password: '1234', 37 | isAdmin: true 38 | })), user => db.model('users').create(user)); 39 | 40 | const seedUsers = () => db.Promise.map(Array(...Array(50)).map(_ => ({ 41 | name: faker.name.findName(), 42 | email: faker.internet.email(), 43 | password: '1234', 44 | organization_name: organizations[rand(organizations.length - 1)].name 45 | })), user => db.model('users').create(user)); 46 | 47 | const seedProblems = () => db.Promise.map(Array(...Array(200)).map(_ => ({ 48 | name: faker.lorem.words(), 49 | description: faker.lorem.sentence(), 50 | difficulty: ['easy', 'medium', 'hard'][rand(2)], 51 | organization_name: organizations[rand(organizations.length - 1)].name, 52 | author_id: rand(11, 60) 53 | })), problem => db.model('problems').create(problem)); 54 | 55 | const seedSolutions = () => db.Promise.map(Array(...Array(400)).map(_ => ({ 56 | code: faker.lorem.paragraph(), 57 | bigO: bigOs[rand(bigOs.length - 1)], 58 | problem_id: rand(1, 200) 59 | })), solution => db.model('solutions').create(solution)); 60 | 61 | const seedInterviews = () => db.Promise.map(Array(...Array(50)).map(_ => ({ 62 | candidateName: faker.name.findName(), 63 | candidateEmail: faker.internet.email(), 64 | interviewer_id: rand(11, 60), 65 | date: faker.date.future(), 66 | position: faker.name.jobType(), 67 | status: ['planned', 'done'][rand(1)], 68 | candidateOverallRating: rand(5), 69 | comments: faker.lorem.sentence() 70 | })), interview => db.model('interviews').create(interview)); 71 | 72 | const randNumGenerator = uniqueRand(200); 73 | 74 | const seedInterviewProblems = () => db.Promise.map(Array(...Array(200)).map((_, i) => ({ 75 | problem_id: randNumGenerator(), 76 | interview_id: rand(1, 50) 77 | })), problem => db.model('interviewProblems').create(problem)); 78 | 79 | db.didSync 80 | .then(() => db.sync({force: true})) 81 | .then(seedOrganizations) 82 | .then(organizations => console.log(`Seeded ${organizations.length} organizations OK`)) 83 | .then(seedAdminUsers) 84 | .then(users => console.log(`Seeded ${users.length} admins OK`)) 85 | .then(seedUsers) 86 | .then(users => console.log(`Seeded ${users.length} users OK`)) 87 | .then(seedProblems) 88 | .then(problems => console.log(`Seeded ${problems.length} problems OK`)) 89 | .then(seedSolutions) 90 | .then(solutions => console.log(`Seeded ${solutions.length} solutions OK`)) 91 | .then(seedInterviews) 92 | .then(interviews => console.log(`Seeded ${interviews.length} interviews OK`)) 93 | .then(seedInterviewProblems) 94 | .then(interviewProblems => console.log(`Seeded ${interviewProblems.length} interviewProblems OK`)) 95 | .catch(error => console.error(error)) 96 | .finally(() => db.close()); 97 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {resolve} = require('path') 4 | const chalk = require('chalk') 5 | const pkg = require('./package.json') 6 | const debug = require('debug')(`${pkg.name}:boot`) 7 | 8 | const nameError = 9 | `******************************************************************* 10 | You need to give your app a proper name. 11 | 12 | The package name 13 | 14 | ${pkg.name} 15 | 16 | isn't valid. If you don't change it, things won't work right. 17 | 18 | Please change it in ${__dirname}/package.json 19 | ~ xoxo, bones 20 | ********************************************************************` 21 | 22 | const reasonableName = /^[a-z0-9\-_]+$/ 23 | if (!reasonableName.test(pkg.name)) { 24 | console.error(chalk.red(nameError)) 25 | } 26 | 27 | // This will load a secrets file from 28 | // 29 | // ~/.your_app_name.env.js 30 | // or ~/.your_app_name.env.json 31 | // 32 | // and add it to the environment. 33 | const env = Object.create(process.env) 34 | , secretsFile = resolve(env.HOME, `.${pkg.name}.env`) 35 | try { 36 | Object.assign(env, require(secretsFile)) 37 | } catch (error) { 38 | debug('%s: %s', secretsFile, error.message) 39 | debug('%s: env file not found or invalid, moving on', secretsFile) 40 | } 41 | 42 | module.exports = { 43 | get name() { return pkg.name }, 44 | get isTesting() { return !!global.it }, 45 | get isProduction() { 46 | return process.env.NODE_ENV === 'production' 47 | }, 48 | get baseUrl() { 49 | return env.BASE_URL || `http://localhost:${PORT}` 50 | }, 51 | get port() { 52 | return env.PORT || 1337 53 | }, 54 | package: pkg, 55 | env, 56 | } 57 | -------------------------------------------------------------------------------- /node_modules/APP: -------------------------------------------------------------------------------- 1 | .. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codehuddle", 3 | "version": "0.0.1", 4 | "description": "A happy little skeleton.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "check-node-version --node '>= 6.7.0' && bin/setup && mocha --compilers js:babel-core/register app/**/*.test.js app/**/*.test.jsx db/**/*.test.js server/**/*.test.js", 8 | "test-watch": "check-node-version --node '>= 6.7.0' && bin/setup && mocha --compilers js:babel-register --watch app/**/*.test.js app/**/*.test.jsx db/**/*.test.js server/**/*.test.js", 9 | "build": "check-node-version --node '>= 6.7.0' && bin/setup && webpack", 10 | "build-sass": "sass --watch public:public/stylesheets", 11 | "build-watch": "check-node-version --node '>= 6.7.0' && bin/setup && webpack -w", 12 | "build-branch": "bin/build-branch.sh", 13 | "deploy-heroku": "bin/deploy-heroku.sh", 14 | "start": "bin/setup && webpack && nodemon server/start.js", 15 | "seed": "node db/seed.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/queerviolet/bones.git" 20 | }, 21 | "keywords": [ 22 | "react", 23 | "redux", 24 | "skeleton" 25 | ], 26 | "author": "Ashi Krishnan ", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/queerviolet/bones/issues" 30 | }, 31 | "homepage": "https://github.com/queerviolet/bones#readme", 32 | "dependencies": { 33 | "articles": "^0.2.1", 34 | "axios": "^0.15.2", 35 | "babel": "^6.5.2", 36 | "babel-core": "^6.18.0", 37 | "babel-loader": "^6.2.7", 38 | "babel-preset-es2015": "^6.18.0", 39 | "babel-preset-react": "^6.16.0", 40 | "babel-preset-stage-2": "^6.18.0", 41 | "babel-register": "^6.22.0", 42 | "bcryptjs": "^2.4.0", 43 | "body-parser": "^1.15.2", 44 | "brace": "^0.9.1", 45 | "chai": "^3.5.0", 46 | "chai-enzyme": "^0.5.2", 47 | "chalk": "^1.1.3", 48 | "check-node-version": "^1.1.2", 49 | "cookie-session": "^2.0.0-alpha.1", 50 | "debug": "^2.6.0", 51 | "enzyme": "^2.5.1", 52 | "express": "^4.14.0", 53 | "faker": "^3.1.0", 54 | "immutable": "^3.8.1", 55 | "konva": "^1.3.0", 56 | "material-ui": "^0.16.7", 57 | "mocha": "^3.1.2", 58 | "node-sass": "^4.3.0", 59 | "nodemon": "^1.11.0", 60 | "passport": "^0.3.2", 61 | "passport-facebook": "^2.1.1", 62 | "passport-github2": "^0.1.10", 63 | "passport-google-oauth": "^1.0.0", 64 | "passport-local": "^1.0.0", 65 | "pg": "^6.1.0", 66 | "pretty-error": "^2.0.2", 67 | "react": "^15.3.2", 68 | "react-ace": "github:runandrew/react-ace", 69 | "react-addons-test-utils": "^15.4.2", 70 | "react-color": "^2.11.1", 71 | "react-dom": "^15.3.2", 72 | "react-konva": "^1.1.1", 73 | "react-line": "^1.0.2", 74 | "react-modal": "^1.6.5", 75 | "react-rating": "^0.6.5", 76 | "react-redux": "^4.4.5", 77 | "react-router": "^3.0.0", 78 | "react-tap-event-plugin": "^2.0.1", 79 | "react-waypoint": "^5.0.3", 80 | "redux": "^3.6.0", 81 | "redux-logger": "^2.7.0", 82 | "redux-thunk": "^2.1.0", 83 | "sequelize": "^3.24.6", 84 | "shortid": "^2.2.6", 85 | "sinon": "^1.17.6", 86 | "sinon-chai": "^2.8.0", 87 | "socket.io": "^1.7.2", 88 | "supertest": "^2.0.1", 89 | "supertest-as-promised": "^4.0.1", 90 | "webpack": "^1.13.3" 91 | }, 92 | "devDependencies": { 93 | "volleyball": "^1.4.1" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /public/_ir-style.scss: -------------------------------------------------------------------------------- 1 | /* Interview Room - Root */ 2 | 3 | #ir-editor-col, 4 | #ir-canvas-col { 5 | padding: 0px; 6 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodehuddleFSA/codehuddle/78c46b30b10c4f81cd8b3975b489f7c635610157/public/favicon.ico -------------------------------------------------------------------------------- /public/images/ace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodehuddleFSA/codehuddle/78c46b30b10c4f81cd8b3975b489f7c635610157/public/images/ace.png -------------------------------------------------------------------------------- /public/images/brush.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/cloud-sync.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/code-background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodehuddleFSA/codehuddle/78c46b30b10c4f81cd8b3975b489f7c635610157/public/images/code-background.jpeg -------------------------------------------------------------------------------- /public/images/code-outline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /public/images/developerpicture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodehuddleFSA/codehuddle/78c46b30b10c4f81cd8b3975b489f7c635610157/public/images/developerpicture.jpg -------------------------------------------------------------------------------- /public/images/hired.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodehuddleFSA/codehuddle/78c46b30b10c4f81cd8b3975b489f7c635610157/public/images/hired.jpg -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Code Huddle 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/style.scss: -------------------------------------------------------------------------------- 1 | /* Imports */ 2 | 3 | @import url('https://fonts.googleapis.com/css?family=Aldrich'); 4 | @import url('https://fonts.googleapis.com/css?family=Allura'); 5 | @import '_ir-style'; 6 | 7 | /* Variables */ 8 | 9 | $splash-bg: #FFF; 10 | $splash-bg-1: #81CCF0; 11 | $header-font: Aldrich; 12 | $sub-header-font: Allura; 13 | 14 | /* Rules */ 15 | 16 | body { 17 | box-sizing: border-box; 18 | margin: 0; 19 | 20 | a:link, a:visited, a:active, a:hover { 21 | color: inherit; 22 | text-decoration: none; 23 | } 24 | a:hover { 25 | text-decoration: underline; 26 | } 27 | 28 | img { 29 | width: 100%; 30 | } 31 | 32 | font-family: Arial; 33 | } 34 | 35 | *, *:after, *:before { 36 | -webkit-box-sizing: border-box; 37 | -moz-box-sizing: border-box; 38 | box-sizing: border-box; 39 | } 40 | 41 | 42 | /* Grid */ 43 | $columnNo: 12; 44 | $gutter: 35px; 45 | $breakpoints: ( 46 | xs: 0px, 47 | sm: 576px, 48 | md: 768px, 49 | lg: 992px, 50 | xl: 1600px, 51 | ); 52 | 53 | .row:before, 54 | .row:after { 55 | content:""; 56 | display: table ; 57 | clear:both; 58 | } 59 | 60 | [class*='col-'] { 61 | float: left; 62 | min-height: 1px; 63 | width: 16.66%; 64 | padding: 0 $gutter/2; 65 | } 66 | 67 | .container { 68 | width: 100%; 69 | max-width: 1200px; 70 | margin: auto; 71 | } 72 | 73 | .right { 74 | float: right; 75 | } 76 | 77 | .row { 78 | margin: 0px; 79 | } 80 | 81 | .center-content { 82 | text-align: center; 83 | } 84 | 85 | // Responsive 86 | @each $name, $breakpoint in $breakpoints { 87 | @media only screen and (min-width: $breakpoint) { 88 | $idx: index($breakpoints, ($name $breakpoint)); 89 | // Grid sizing 90 | @for $i from 1 through 12 { 91 | .col-#{$name}-#{$i} { 92 | width: $i/$columnNo * 100%; 93 | } 94 | } 95 | 96 | } 97 | } 98 | 99 | 100 | /* Splash specific pages */ 101 | #header { 102 | #header-icon { 103 | height: 64px; 104 | width: 64px; 105 | animation-delay: .3s; 106 | margin-top: 250px; 107 | } 108 | /* Splash Content */ 109 | 110 | h1 { 111 | font-family: $header-font; 112 | animation-delay: .4s; 113 | font-size: 4rem; 114 | } 115 | 116 | h4 { 117 | font-family: $sub-header-font; 118 | animation-delay: .8s; 119 | font-size: 2rem; 120 | } 121 | 122 | min-height: 850px; 123 | height: 100vh; 124 | } 125 | 126 | #hipster { 127 | height: 500px; 128 | background: url('/images/developerpicture.jpg'); 129 | background-size: cover; 130 | background-position: 50% 50%; 131 | } 132 | 133 | #info { 134 | background-color: $splash-bg-1; 135 | 136 | h2 { 137 | font-family: $header-font; 138 | font-size: 2rem; 139 | } 140 | } 141 | 142 | #code { 143 | height: 400px; 144 | background: url('/images/code-background.jpeg'); 145 | background-size: cover; 146 | background-position: 50% 50%; 147 | } 148 | 149 | footer { 150 | padding: 1em; 151 | } 152 | 153 | nav { 154 | padding-top: 1em; 155 | font-family: $header-font; 156 | } 157 | 158 | .text-space-leftRight { 159 | padding-left: 1rem; 160 | padding-right: 1rem; 161 | } 162 | 163 | .text-space-upDown { 164 | padding-top: .5rem; 165 | padding-bottom: .5rem; 166 | } 167 | 168 | .row-padding { 169 | padding-top: 3rem; 170 | padding-bottom: 3rem; 171 | } 172 | 173 | .container { 174 | width: 100%; 175 | max-width: 1200px; 176 | margin: auto; 177 | } 178 | 179 | #whiteboard { 180 | border: solid black 1px; 181 | } 182 | 183 | .editor-highlight { 184 | background-color: pink; 185 | position: absolute; 186 | } 187 | 188 | fieldset { 189 | border: none; 190 | margin: 0; 191 | padding-left: 1rem; 192 | } 193 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* Imports */ 2 | @import url("https://fonts.googleapis.com/css?family=Aldrich"); 3 | @import url("https://fonts.googleapis.com/css?family=Allura"); 4 | /* Interview Room - Root */ 5 | #ir-editor-col, 6 | #ir-canvas-col { 7 | padding: 0px; } 8 | 9 | /* Variables */ 10 | /* Rules */ 11 | body { 12 | box-sizing: border-box; 13 | margin: 0; 14 | font-family: Arial; } 15 | body a:link, body a:visited, body a:active, body a:hover { 16 | color: inherit; 17 | text-decoration: none; } 18 | body a:hover { 19 | text-decoration: underline; } 20 | body img { 21 | width: 100%; } 22 | 23 | *, *:after, *:before { 24 | -webkit-box-sizing: border-box; 25 | -moz-box-sizing: border-box; 26 | box-sizing: border-box; } 27 | 28 | /* Grid */ 29 | .row:before, 30 | .row:after { 31 | content: ""; 32 | display: table; 33 | clear: both; } 34 | 35 | [class*='col-'] { 36 | float: left; 37 | min-height: 1px; 38 | width: 16.66%; 39 | padding: 0 17.5px; } 40 | 41 | .container { 42 | width: 100%; 43 | max-width: 1200px; 44 | margin: auto; } 45 | 46 | .right { 47 | float: right; } 48 | 49 | .row { 50 | margin: 0px; } 51 | 52 | .center-content { 53 | text-align: center; } 54 | 55 | @media only screen and (min-width: 0px) { 56 | .col-xs-1 { 57 | width: 8.33333%; } 58 | 59 | .col-xs-2 { 60 | width: 16.66667%; } 61 | 62 | .col-xs-3 { 63 | width: 25%; } 64 | 65 | .col-xs-4 { 66 | width: 33.33333%; } 67 | 68 | .col-xs-5 { 69 | width: 41.66667%; } 70 | 71 | .col-xs-6 { 72 | width: 50%; } 73 | 74 | .col-xs-7 { 75 | width: 58.33333%; } 76 | 77 | .col-xs-8 { 78 | width: 66.66667%; } 79 | 80 | .col-xs-9 { 81 | width: 75%; } 82 | 83 | .col-xs-10 { 84 | width: 83.33333%; } 85 | 86 | .col-xs-11 { 87 | width: 91.66667%; } 88 | 89 | .col-xs-12 { 90 | width: 100%; } } 91 | @media only screen and (min-width: 576px) { 92 | .col-sm-1 { 93 | width: 8.33333%; } 94 | 95 | .col-sm-2 { 96 | width: 16.66667%; } 97 | 98 | .col-sm-3 { 99 | width: 25%; } 100 | 101 | .col-sm-4 { 102 | width: 33.33333%; } 103 | 104 | .col-sm-5 { 105 | width: 41.66667%; } 106 | 107 | .col-sm-6 { 108 | width: 50%; } 109 | 110 | .col-sm-7 { 111 | width: 58.33333%; } 112 | 113 | .col-sm-8 { 114 | width: 66.66667%; } 115 | 116 | .col-sm-9 { 117 | width: 75%; } 118 | 119 | .col-sm-10 { 120 | width: 83.33333%; } 121 | 122 | .col-sm-11 { 123 | width: 91.66667%; } 124 | 125 | .col-sm-12 { 126 | width: 100%; } } 127 | @media only screen and (min-width: 768px) { 128 | .col-md-1 { 129 | width: 8.33333%; } 130 | 131 | .col-md-2 { 132 | width: 16.66667%; } 133 | 134 | .col-md-3 { 135 | width: 25%; } 136 | 137 | .col-md-4 { 138 | width: 33.33333%; } 139 | 140 | .col-md-5 { 141 | width: 41.66667%; } 142 | 143 | .col-md-6 { 144 | width: 50%; } 145 | 146 | .col-md-7 { 147 | width: 58.33333%; } 148 | 149 | .col-md-8 { 150 | width: 66.66667%; } 151 | 152 | .col-md-9 { 153 | width: 75%; } 154 | 155 | .col-md-10 { 156 | width: 83.33333%; } 157 | 158 | .col-md-11 { 159 | width: 91.66667%; } 160 | 161 | .col-md-12 { 162 | width: 100%; } } 163 | @media only screen and (min-width: 992px) { 164 | .col-lg-1 { 165 | width: 8.33333%; } 166 | 167 | .col-lg-2 { 168 | width: 16.66667%; } 169 | 170 | .col-lg-3 { 171 | width: 25%; } 172 | 173 | .col-lg-4 { 174 | width: 33.33333%; } 175 | 176 | .col-lg-5 { 177 | width: 41.66667%; } 178 | 179 | .col-lg-6 { 180 | width: 50%; } 181 | 182 | .col-lg-7 { 183 | width: 58.33333%; } 184 | 185 | .col-lg-8 { 186 | width: 66.66667%; } 187 | 188 | .col-lg-9 { 189 | width: 75%; } 190 | 191 | .col-lg-10 { 192 | width: 83.33333%; } 193 | 194 | .col-lg-11 { 195 | width: 91.66667%; } 196 | 197 | .col-lg-12 { 198 | width: 100%; } } 199 | @media only screen and (min-width: 1600px) { 200 | .col-xl-1 { 201 | width: 8.33333%; } 202 | 203 | .col-xl-2 { 204 | width: 16.66667%; } 205 | 206 | .col-xl-3 { 207 | width: 25%; } 208 | 209 | .col-xl-4 { 210 | width: 33.33333%; } 211 | 212 | .col-xl-5 { 213 | width: 41.66667%; } 214 | 215 | .col-xl-6 { 216 | width: 50%; } 217 | 218 | .col-xl-7 { 219 | width: 58.33333%; } 220 | 221 | .col-xl-8 { 222 | width: 66.66667%; } 223 | 224 | .col-xl-9 { 225 | width: 75%; } 226 | 227 | .col-xl-10 { 228 | width: 83.33333%; } 229 | 230 | .col-xl-11 { 231 | width: 91.66667%; } 232 | 233 | .col-xl-12 { 234 | width: 100%; } } 235 | /* Splash specific pages */ 236 | #header { 237 | /* Splash Content */ 238 | min-height: 850px; 239 | height: 100vh; } 240 | #header #header-icon { 241 | height: 64px; 242 | width: 64px; 243 | animation-delay: .3s; 244 | margin-top: 250px; } 245 | #header h1 { 246 | font-family: Aldrich; 247 | animation-delay: .4s; 248 | font-size: 4rem; } 249 | #header h4 { 250 | font-family: Allura; 251 | animation-delay: .8s; 252 | font-size: 2rem; } 253 | 254 | #hipster { 255 | height: 500px; 256 | background: url("/images/developerpicture.jpg"); 257 | background-size: cover; 258 | background-position: 50% 50%; } 259 | 260 | #info { 261 | background-color: #81CCF0; } 262 | #info h2 { 263 | font-family: Aldrich; 264 | font-size: 2rem; } 265 | 266 | #code { 267 | height: 400px; 268 | background: url("/images/code-background.jpeg"); 269 | background-size: cover; 270 | background-position: 50% 50%; } 271 | 272 | footer { 273 | padding: 1em; } 274 | 275 | nav { 276 | padding-top: 1em; 277 | font-family: Aldrich; } 278 | 279 | .text-space-leftRight { 280 | padding-left: 1rem; 281 | padding-right: 1rem; } 282 | 283 | .text-space-upDown { 284 | padding-top: .5rem; 285 | padding-bottom: .5rem; } 286 | 287 | .row-padding { 288 | padding-top: 3rem; 289 | padding-bottom: 3rem; } 290 | 291 | .container { 292 | width: 100%; 293 | max-width: 1200px; 294 | margin: auto; } 295 | 296 | #whiteboard { 297 | border: solid black 1px; } 298 | 299 | .editor-highlight { 300 | background-color: pink; 301 | position: absolute; } 302 | 303 | fieldset { 304 | border: none; 305 | margin: 0; 306 | padding-left: 1rem; } 307 | 308 | /*# sourceMappingURL=style.css.map */ 309 | -------------------------------------------------------------------------------- /public/stylesheets/style.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,cAAc;AAEN,8DAAsD;AACtD,6DAAqD;ACH7D,2BAA2B;AAE3B;cACe;EACd,OAAO,EAAE,GAAG;;ADEb,eAAe;AAOf,WAAW;AAEX,IAAK;EACH,UAAU,EAAE,UAAU;EACtB,MAAM,EAAE,CAAC;EAcT,WAAW,EAAE,KAAK;EAZlB,wDAAqC;IACnC,KAAK,EAAE,OAAO;IACd,eAAe,EAAE,IAAI;EAEvB,YAAQ;IACN,eAAe,EAAE,SAAS;EAG5B,QAAI;IACF,KAAK,EAAE,IAAI;;AAMf,oBAAqB;EACnB,kBAAkB,EAAE,UAAU;EAC9B,eAAe,EAAE,UAAU;EAC3B,UAAU,EAAE,UAAU;;AAIxB,UAAU;AAWV;UACW;EACT,OAAO,EAAC,EAAE;EACV,OAAO,EAAE,KAAK;EACd,KAAK,EAAC,IAAI;;AAGZ,eAAgB;EACd,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,GAAG;EACf,KAAK,EAAE,MAAM;EACb,OAAO,EAAE,QAAW;;AAGtB,UAAW;EACT,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,MAAM;EACjB,MAAM,EAAE,IAAI;;AAGd,MAAO;EACL,KAAK,EAAE,KAAK;;AAGd,IAAK;EACH,MAAM,EAAE,GAAG;;AAGb,eAAgB;EACd,UAAU,EAAE,MAAM;;AAKlB,uCAAgD;EAI5C,SAAoB;IAClB,KAAK,EAAE,QAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,IAAmB;AALhC,yCAAgD;EAI5C,SAAoB;IAClB,KAAK,EAAE,QAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,IAAmB;AALhC,yCAAgD;EAI5C,SAAoB;IAClB,KAAK,EAAE,QAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,IAAmB;AALhC,yCAAgD;EAI5C,SAAoB;IAClB,KAAK,EAAE,QAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,IAAmB;AALhC,0CAAgD;EAI5C,SAAoB;IAClB,KAAK,EAAE,QAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,SAAoB;IAClB,KAAK,EAAE,GAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,SAAmB;;EAD5B,UAAoB;IAClB,KAAK,EAAE,IAAmB;AAQlC,2BAA2B;AAC3B,OAAQ;EAOR,oBAAoB;EAclB,UAAU,EAAE,KAAK;EACjB,MAAM,EAAE,KAAK;EArBb,oBAAa;IACX,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,eAAe,EAAE,GAAG;IACpB,UAAU,EAAE,KAAK;EAInB,UAAG;IACD,WAAW,EApGD,OAAO;IAqGjB,eAAe,EAAE,GAAG;IACpB,SAAS,EAAE,IAAI;EAGjB,UAAG;IACD,WAAW,EAzGG,MAAM;IA0GpB,eAAe,EAAE,GAAG;IACpB,SAAS,EAAE,IAAI;;AAOnB,QAAS;EACP,MAAM,EAAE,KAAK;EACb,UAAU,EAAE,mCAAmC;EAC/C,eAAe,EAAE,KAAK;EACtB,mBAAmB,EAAE,OAAO;;AAG9B,KAAM;EACJ,gBAAgB,EA5HJ,OAAO;EA8HnB,QAAG;IACD,WAAW,EA9HD,OAAO;IA+HjB,SAAS,EAAE,IAAI;;AAInB,KAAM;EACJ,MAAM,EAAE,KAAK;EACb,UAAU,EAAE,mCAAmC;EAC/C,eAAe,EAAE,KAAK;EACtB,mBAAmB,EAAE,OAAO;;AAG9B,MAAO;EACL,OAAO,EAAE,GAAG;;AAGd,GAAI;EACF,WAAW,EAAE,GAAG;EAChB,WAAW,EAhJC,OAAO;;AAmJrB,qBAAsB;EACpB,YAAY,EAAE,IAAI;EAClB,aAAa,EAAE,IAAI;;AAGrB,kBAAmB;EACjB,WAAW,EAAE,KAAK;EAClB,cAAc,EAAE,KAAK;;AAGvB,YAAa;EACX,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;;AAGtB,UAAW;EACP,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,MAAM;EACjB,MAAM,EAAE,IAAI;;AAGhB,WAAY;EACV,MAAM,EAAE,eAAe;;AAGzB,iBAAkB;EAChB,gBAAgB,EAAE,IAAI;EACtB,QAAQ,EAAE,QAAQ;;AAGpB,QAAS;EACP,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,CAAC;EACT,YAAY,EAAE,IAAI", 4 | "sources": ["../style.scss","../_ir-style.scss"], 5 | "names": [], 6 | "file": "style.css" 7 | } -------------------------------------------------------------------------------- /server/api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const db = require('APP/db') 4 | const api = module.exports = require('express').Router() 5 | 6 | api 7 | .get('/heartbeat', (req, res) => res.send({ok: true})) 8 | .use('/auth', require('./auth')) 9 | .use('/users', require('./users')) 10 | .use('/interviews', require('./interviews')) 11 | .use('/interviewProblems', require('./interviewProblems')) 12 | .use('/problems', require('./problems')) 13 | .use('/organizations', require('./organizations')); 14 | // No routes matched? 404. 15 | api.use((req, res) => res.status(404).end()) 16 | -------------------------------------------------------------------------------- /server/auth.filters.js: -------------------------------------------------------------------------------- 1 | const mustBeLoggedIn = (req, res, next) => { 2 | if (!req.user) { 3 | return res.status(401).send('You must be logged in'); 4 | } 5 | next(); 6 | }; 7 | 8 | const selfOnly = (req, res, next) => { 9 | if (req.params.userId !== req.user.id) { 10 | return res.status(403).send('Only user can take this action.'); 11 | } 12 | next(); 13 | }; 14 | 15 | const forbidden = message => (req, res, next) => { 16 | res.status(403).send(message) 17 | } 18 | 19 | module.exports = {mustBeLoggedIn, selfOnly, forbidden}; 20 | -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const passport = require('passport'); 3 | const LocalStrategy = require('passport-local').Strategy; 4 | const db = require('APP/db'); 5 | const User = db.model('users'); 6 | 7 | passport.serializeUser((user, done) => { 8 | done(null, user.id); 9 | }); 10 | 11 | passport.deserializeUser((id, done) => { 12 | User.findById(id) 13 | .then(user => { 14 | done(null, user); 15 | }) 16 | .catch(done); 17 | }); 18 | 19 | passport.use(new LocalStrategy((email, password, done) => { 20 | User.findOne({where: {email}}) 21 | .then(user => { 22 | if (!user) { 23 | return done(null, false, { message: 'Login incorrect' }) 24 | } 25 | return user.authenticate(password) 26 | .then(ok => { 27 | if (!ok) { 28 | return done(null, false, { message: 'Login incorrect' }) 29 | } 30 | done(null, user) 31 | }); 32 | }) 33 | .catch(done); 34 | })); 35 | 36 | router.post('/login', passport.authenticate('local', {successRedirect: '/'})); 37 | 38 | router.get('/whoami', (req, res) => res.send(req.user)); 39 | 40 | router.post('/logout', (req, res, next) => { 41 | req.logout(); 42 | // not necessary just being extra cautious 43 | res.redirect('/api/auth/whoami'); 44 | }); 45 | 46 | module.exports = router; 47 | -------------------------------------------------------------------------------- /server/auth.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const app = require('./start') 6 | 7 | const ben = { 8 | username: 'ben@test.com', 9 | password: '12345' 10 | } 11 | 12 | describe('/api/auth', () => { 13 | before('create a user', () => 14 | db.didSync 15 | .then(() => 16 | User.create({ 17 | email: ben.username, 18 | password: ben.password 19 | }) 20 | ) 21 | ); 22 | 23 | describe('POST /local/login (username, password)', () => { 24 | it('succeeds with a valid username and password', () => 25 | request(app) 26 | .post('/api/auth/login') 27 | .send(ben) 28 | .expect(302) 29 | .expect('Set-Cookie', /session=.*/) 30 | .expect('Location', '/') 31 | ) 32 | 33 | it('fails with an invalid username and password', () => 34 | request(app) 35 | .post('/api/auth/login') 36 | .send({username: ben.username, password: 'wrong'}) 37 | .expect(401) 38 | ) 39 | }) 40 | 41 | describe('GET /whoami', () => { 42 | describe('when logged in,', () => { 43 | const agent = request.agent(app) 44 | before('log in', () => agent 45 | .post('/api/auth/login') 46 | .send(ben)) 47 | 48 | it('responds with the currently logged in user', () => 49 | agent.get('/api/auth/whoami') 50 | .set('Accept', 'application/json') 51 | .expect(200) 52 | .then(res => expect(res.body).to.contain({ 53 | email: ben.username 54 | })) 55 | ) 56 | }) 57 | 58 | it('when not logged in, responds with an empty object', () => 59 | request(app).get('/api/auth/whoami') 60 | .expect(200) 61 | .then(res => expect(res.body).to.eql({})) 62 | ) 63 | }) 64 | 65 | describe('POST /logout when logged in', () => { 66 | const agent = request.agent(app); 67 | 68 | before('log in', () => agent 69 | .post('/api/auth/login') 70 | .send(ben)); 71 | 72 | it('logs you out and redirects to whoami', () => agent 73 | .post('/api/auth/logout') 74 | .expect(302) 75 | .expect('Location', '/api/auth/whoami') 76 | .then(() => 77 | agent.get('/api/auth/whoami') 78 | .expect(200) 79 | .then(rsp => expect(rsp.body).eql({})) 80 | ) 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /server/bonesAuth.js: -------------------------------------------------------------------------------- 1 | const app = require('APP'), {env} = app 2 | const debug = require('debug')(`${app.name}:auth`) 3 | const passport = require('passport') 4 | 5 | const User = require('APP/db/models/user') 6 | const OAuth = require('APP/db/models/oauth') 7 | const auth = require('express').Router() 8 | 9 | /************************* 10 | * Auth strategies 11 | * 12 | * The OAuth model knows how to configure Passport middleware. 13 | * To enable an auth strategy, ensure that the appropriate 14 | * environment variables are set. 15 | * 16 | * You can do it on the command line: 17 | * 18 | * FACEBOOK_CLIENT_ID=abcd FACEBOOK_CLIENT_SECRET=1234 npm start 19 | * 20 | * Or, better, you can create a ~/.$your_app_name.env.json file in 21 | * your home directory, and set them in there: 22 | * 23 | * { 24 | * FACEBOOK_CLIENT_ID: 'abcd', 25 | * FACEBOOK_CLIENT_SECRET: '1234', 26 | * } 27 | * 28 | * Concentrating your secrets this way will make it less likely that you 29 | * accidentally push them to Github, for example. 30 | * 31 | * When you deploy to production, you'll need to set up these environment 32 | * variables with your hosting provider. 33 | **/ 34 | 35 | // Facebook needs the FACEBOOK_CLIENT_ID and FACEBOOK_CLIENT_SECRET 36 | // environment variables. 37 | OAuth.setupStrategy({ 38 | provider: 'facebook', 39 | strategy: require('passport-facebook').Strategy, 40 | config: { 41 | clientID: env.FACEBOOK_CLIENT_ID, 42 | clientSecret: env.FACEBOOK_CLIENT_SECRET, 43 | callbackURL: `${app.rootUrl}/api/auth/login/facebook`, 44 | }, 45 | passport 46 | }) 47 | 48 | // Google needs the GOOGLE_CONSUMER_SECRET AND GOOGLE_CONSUMER_KEY 49 | // environment variables. 50 | OAuth.setupStrategy({ 51 | provider: 'google', 52 | strategy: require('passport-google-oauth').Strategy, 53 | config: { 54 | consumerKey: env.GOOGLE_CONSUMER_KEY, 55 | consumerSecret: env.GOOGLE_CONSUMER_SECRET, 56 | callbackURL: `${app.rootUrl}/api/auth/login/google`, 57 | }, 58 | passport 59 | }) 60 | 61 | // Github needs the GITHUB_CLIENT_ID AND GITHUB_CLIENT_SECRET 62 | // environment variables. 63 | OAuth.setupStrategy({ 64 | provider: 'github', 65 | strategy: require('passport-github2').Strategy, 66 | config: { 67 | clientID: env.GITHUB_CLIENT_ID, 68 | clientSecrets: env.GITHUB_CLIENT_SECRET, 69 | callbackURL: `${app.rootUrl}/api/auth/login/github`, 70 | }, 71 | passport 72 | }) 73 | 74 | // Other passport configuration: 75 | 76 | passport.serializeUser((user, done) => { 77 | debug('will serialize user.id=%d', user.id) 78 | done(null, user.id) 79 | debug('did serialize user.id=%d', user.id) 80 | }) 81 | 82 | passport.deserializeUser( 83 | (id, done) => { 84 | debug('will deserialize user.id=%d', id) 85 | User.findById(id) 86 | .then(user => { 87 | debug('deserialize did ok user.id=%d', user.id) 88 | done(null, user) 89 | }) 90 | .catch(err => { 91 | debug('deserialize did fail err=%s', err) 92 | done(err) 93 | }) 94 | } 95 | ) 96 | 97 | passport.use(new (require('passport-local').Strategy) ( 98 | (email, password, done) => { 99 | debug('will authenticate user(email: "%s")', email) 100 | User.findOne({where: {email}}) 101 | .then(user => { 102 | if (!user) { 103 | debug('authenticate user(email: "%s") did fail: no such user', email) 104 | return done(null, false, { message: 'Login incorrect' }) 105 | } 106 | return user.authenticate(password) 107 | .then(ok => { 108 | if (!ok) { 109 | debug('authenticate user(email: "%s") did fail: bad password') 110 | return done(null, false, { message: 'Login incorrect' }) 111 | } 112 | debug('authenticate user(email: "%s") did ok: user.id=%d', user.id) 113 | done(null, user) 114 | }) 115 | }) 116 | .catch(done) 117 | } 118 | )) 119 | 120 | auth.get('/whoami', (req, res) => res.send(req.user)) 121 | 122 | auth.post('/:strategy/login', (req, res, next) => 123 | passport.authenticate(req.params.strategy, { 124 | successRedirect: '/' 125 | })(req, res, next) 126 | ); 127 | 128 | auth.post('/logout', (req, res, next) => { 129 | req.logout(); 130 | res.redirect('/api/auth/whoami'); 131 | }); 132 | 133 | module.exports = auth 134 | -------------------------------------------------------------------------------- /server/bonesUsers.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest-as-promised') 2 | const {expect} = require('chai') 3 | const db = require('APP/db') 4 | const User = require('APP/db/models/user') 5 | const app = require('./start') 6 | 7 | describe('/api/users', () => { 8 | xdescribe('when not logged in', () => { 9 | it('GET /:id fails 401 (Unauthorized)', () => 10 | request(app) 11 | .get(`/api/users/1`) 12 | .expect(401) 13 | ) 14 | 15 | it('POST creates a user', () => 16 | request(app) 17 | .post('/api/users') 18 | .send({ 19 | email: 'beth@secrets.org', 20 | password: '12345' 21 | }) 22 | .expect(201) 23 | ) 24 | 25 | it('POST redirects to the user it just made', () => 26 | request(app) 27 | .post('/api/users') 28 | .send({ 29 | email: 'eve@interloper.com', 30 | password: '23456' 31 | }) 32 | .redirects(1) 33 | .then(res => expect(res.body).to.contain({ 34 | email: 'eve@interloper.com' 35 | })) 36 | ) 37 | }) 38 | }) -------------------------------------------------------------------------------- /server/interviewProblems.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const db = require('APP/db'); 3 | const InterviewProblem = db.model('interviewProblems'); 4 | 5 | router.put('/:interviewId/problems/:problemId', (req, res, next) => { 6 | InterviewProblem.update(req.body, { 7 | where: { 8 | interview_id: req.params.interviewId, 9 | problem_id: req.params.problemId 10 | } 11 | }) 12 | .then(_ => res.send(_)) 13 | .catch(next); 14 | }); 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /server/interviews.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const db = require('APP/db'); 3 | const Interview = db.model('interviews'); 4 | const InterviewProblem = db.model('interviewProblems'); 5 | const Solution = db.model('solutions'); 6 | 7 | // TODO: check authorizations where necessary. Can we do it in router.param? 8 | 9 | router.get('/', (req, res, next) => { 10 | req.user.getInterviews() 11 | .then(response => { 12 | if (response) res.json(response); 13 | else res.sendStatus(404); 14 | }) 15 | .catch(next); 16 | }); 17 | 18 | router.post('/', (req, res, next) => { 19 | Interview.create(req.body) 20 | .then(createdInterview => res.status(201).send(createdInterview)) 21 | .catch(next); 22 | }); 23 | 24 | router.param('interviewId', (req, res, next, id) => { 25 | Interview.findById(id) 26 | .then(interview => { 27 | if (!interview) res.send(404); 28 | else { 29 | req.interview = interview; 30 | next(); 31 | } 32 | }) 33 | .catch(next); 34 | }); 35 | 36 | router.get('/:interviewId', (req, res, next) => { 37 | res.send(req.interview); 38 | }); 39 | 40 | router.put('/:interviewId', (req, res, next) => { 41 | req.interview.update(req.body) 42 | .then(interview => res.send(interview)) 43 | .catch(next); 44 | }); 45 | 46 | router.delete('/:interviewId', (req, res, next) => { 47 | req.interview.destroy() 48 | .then(() => res.sendStatus(204)) 49 | .catch(next); 50 | }); 51 | 52 | router.get('/:interviewId/problems', (req, res, next) => { 53 | req.interview.getProblems({include: [Solution]}) 54 | .then(problems => res.send(problems)) 55 | .catch(next); 56 | }); 57 | 58 | router.post('/:interviewId/problems', (req, res, next) => { 59 | req.interview.addProblem(req.body.problemId) 60 | .then(problem => res.status(201).send(problem)) 61 | .catch(next); 62 | }); 63 | 64 | router.delete('/:interviewId/problems/:problemId', (req, res, next) => { 65 | req.interview.removeProblem(req.params.problemId) 66 | .then(() => res.sendStatus(204)) 67 | .catch(next); 68 | }); 69 | 70 | module.exports = router; 71 | -------------------------------------------------------------------------------- /server/interviews.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const {expect} = require('chai'); 3 | const User = require('APP/db/models/user'); 4 | const Interview = require('APP/db/models/interview'); 5 | const Problem = require('APP/db/models/problem'); 6 | const app = require('./start'); 7 | 8 | describe('interviews router', () => { 9 | let marla; 10 | let createdInterview; 11 | before('create interviews', () => 12 | User.create({ 13 | name: 'Marla', 14 | email: 'marla@test.com', 15 | password: '1234' 16 | }) 17 | .then(user => { 18 | marla = user; 19 | }) 20 | .then(() => Interview.create({ 21 | date: '1/30/2017', 22 | interviewer_id: marla.id, 23 | problems: [ 24 | {name: 'Bitwise Add', difficulty: 'medium'}, 25 | {name: 'Sudoku Validator', difficulty: 'medium'} 26 | ] 27 | }, { 28 | include: [Problem] 29 | })) 30 | .then(interview => { 31 | createdInterview = interview; 32 | }) 33 | ); 34 | 35 | describe('POST /api/interviews', () => { 36 | it('creates an interview', () => 37 | request(app) 38 | .post('/api/interviews') 39 | .send({ 40 | date: '1/30/2017', 41 | interviewer_id: createdInterview.interviewer_id, 42 | position: 'Engineer' 43 | }) 44 | .expect(201) 45 | .then(res => expect(res.body.position).to.equal('Engineer')) 46 | ); 47 | }); 48 | 49 | describe('GET /api/interviews/:interviewId', () => { 50 | it('gets an interview by id', () => 51 | request(app) 52 | .get(`/api/interviews/${createdInterview.id}`) 53 | .expect(200) 54 | .then(res => expect(res.body.candidate_id).to.equal(createdInterview.candidate_id)) 55 | ); 56 | }); 57 | 58 | describe('PUT /api/interviews/:interviewId', () => { 59 | it('updates given interview', () => 60 | request(app) 61 | .put(`/api/interviews/${createdInterview.id}`) 62 | .send({position: 'Manager'}) 63 | .expect(200) 64 | .then(res => expect(res.body.position).to.equal('Manager')) 65 | ); 66 | }); 67 | 68 | describe('GET /api/interviews/:interviewId/problems', () => { 69 | it('gets all problems of given interview', () => 70 | request(app) 71 | .get(`/api/interviews/${createdInterview.id}/problems`) 72 | .expect(200) 73 | .then(res => expect(res.body).to.have.lengthOf(2)) 74 | ); 75 | }); 76 | 77 | describe('/api/interviews/:interviewId/problems', () => { 78 | let interview, problem; 79 | before(() => 80 | Interview.create({ 81 | date: '1/30/2017', 82 | interviewer_id: marla.id 83 | }) 84 | .then(i => { 85 | interview = i; 86 | }) 87 | .then(() => Problem.create({ 88 | name: 'Decimal To Binary', 89 | difficulty: 'easy' 90 | })) 91 | .then(p => { 92 | problem = p; 93 | }) 94 | .catch(err => console.log('create error: ', err)) 95 | ); 96 | 97 | describe('POST /api/interviews/:interviewId/problems', () => { 98 | it('adds a problem to an interview', () => 99 | request(app) 100 | .post(`/api/interviews/${interview.id}/problems`) 101 | .send({problemId: problem.id}) 102 | .expect(201) 103 | .then(() => 104 | Interview.findById(interview.id, { 105 | include: [Problem] 106 | }) 107 | ) 108 | .then(interview => expect(interview.problems).to.have.lengthOf(1)) 109 | ); 110 | }); 111 | 112 | describe('DELETE /api/interviews/:interviewId/problems/:problemId', () => { 113 | it('removes given problem from interview', () => 114 | request(app) 115 | .delete(`/api/interviews/${interview.id}/problems/${problem.id}`) 116 | .expect(204) 117 | .then(() => Interview.findById(interview.id, { 118 | include: [Problem] 119 | })) 120 | .then(interview => expect(interview.problems).to.have.lengthOf(0)) 121 | ); 122 | }); 123 | }); 124 | 125 | describe('DELETE /api/interviews/:interviewId', () => { 126 | it('deletes given interview', () => 127 | request(app) 128 | .delete(`/api/interviews/${createdInterview.id}`) 129 | .expect(204) 130 | .then(() => Interview.findById(createdInterview.id)) 131 | .then(interview => expect(interview).to.be.null) 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /server/organizations.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const db = require('APP/db'); 3 | const Organization = db.model('organizations'); 4 | const User = db.model('users'); 5 | const Problems = db.model('problems'); 6 | 7 | router.get('/', (req, res, next) => { 8 | Organization.findAll() 9 | .then(organizations => res.send(organizations)) 10 | .catch(next); 11 | }); 12 | 13 | router.post('/', (req, res, next) => { 14 | Organization.create(req.body) 15 | .then(() => res.sendStatus(201)) 16 | .catch(next); 17 | }); 18 | 19 | router.param('organizationSlug', (req, res, next, slug) => { 20 | Organization.findById(slug) 21 | .then(organization => { 22 | if (!organization) res.sendStatus(404); 23 | else { 24 | req.organization = organization; 25 | next() 26 | } 27 | }) 28 | .catch(next); 29 | }); 30 | 31 | router.delete('/:organizationSlug', (req, res, next) => { 32 | req.organization.destroy() 33 | .then(response => { 34 | if (response === 0) res.sendStatus(404); 35 | else res.sendStatus(204); 36 | }) 37 | .catch(next); 38 | }); 39 | 40 | router.get('/:organizationSlug/problems', (req, res, next) => { 41 | req.organization.getProblems() 42 | .then(problems => res.send(problems)) 43 | .catch(next); 44 | }); 45 | 46 | router.get('/:organizationSlug/users', (req, res, next) => { 47 | req.organization.getUsers() 48 | .then(users => res.send(users)) 49 | .catch(next); 50 | }); 51 | 52 | // Is this route necessary? 53 | // router.get('/:organizationSlug/interviews', (req, res, next) => { 54 | 55 | // }); 56 | 57 | module.exports = router; 58 | -------------------------------------------------------------------------------- /server/organizations.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const {expect} = require('chai'); 3 | const User = require('APP/db/models/user'); 4 | const Organization = require('APP/db/models/organization'); 5 | const app = require('./start'); 6 | 7 | describe('organizations router', () => { 8 | before(() => { 9 | const organizations = [ 10 | {name: 'Google'}, 11 | {name: 'Facebook'}, 12 | {name: 'Amazon'}, 13 | {name: 'Dropbox'} 14 | ]; 15 | 16 | const users = [ 17 | {name: 'Mark', email: 'mark@test.com', password: '1234', organization_name: 'Google'}, 18 | {name: 'Evan', email: 'evan@test.com', password: '1234', organization_name: 'Google'} 19 | ]; 20 | 21 | // promise.all or separate before blocks 22 | Organization.bulkCreate(organizations) 23 | .then(() => 24 | User.bulkCreate(users)); 25 | 26 | User.create({ 27 | name: 'Mike', 28 | email: 'mike@test.com', 29 | password: '3456', 30 | organization: { 31 | name: 'American Express' 32 | } 33 | }, { 34 | include: [Organization] 35 | }); 36 | }); 37 | 38 | describe('POST /api/organizations', () => { 39 | it('creates a new organization', () => 40 | request(app) 41 | .post('/api/organizations') 42 | .send({name: 'Etsy'}) 43 | .expect(201) 44 | ); 45 | }); 46 | 47 | describe('GET /api/organizations', () => { 48 | it('responds with all organizations', () => 49 | request(app) 50 | .get('/api/organizations') 51 | .expect(200) 52 | .then(res => { 53 | expect(res.body).to.have.lengthOf(6); 54 | }) 55 | ); 56 | }); 57 | 58 | describe('GET /api/:organizationName/organizations/users', () => { 59 | it('responds with all users of an organization', () => 60 | request(app) 61 | .get('/api/organizations/Google/users') 62 | .expect(200) 63 | .then(res => { 64 | expect(res.body).to.have.lengthOf(2); 65 | }) 66 | ); 67 | }) 68 | 69 | describe('DELETE /api/organizations/:organizationName', () => { 70 | it('it deletes the organization if it exists', () => 71 | request(app) 72 | .delete('/api/organizations/Google') 73 | .expect(204) 74 | ); 75 | 76 | it('responds with 404 if organization does not exist', () => 77 | request(app) 78 | .delete('/api/organizations/Priceline') 79 | .expect(404) 80 | ); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /server/problems.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const db = require('APP/db'); 3 | const Problem = db.model('problems'); 4 | 5 | router.post('/', (req, res, next) => { 6 | Problem.create(req.body) 7 | .then(createdProblem => res.status(201).send(createdProblem)) 8 | .catch(next); 9 | }); 10 | 11 | router.param('problemId', (req, res, next, id) => { 12 | Problem.findById(id) 13 | .then(problem => { 14 | if (!problem) res.send(404); 15 | else { 16 | req.problem = problem; 17 | next(); 18 | } 19 | }) 20 | .catch(next); 21 | }); 22 | 23 | router.get('/:problemId', (req, res, next) => { 24 | res.send(req.problem); 25 | }); 26 | 27 | router.put('/:problemId', (req, res, next) => { 28 | req.problem.update(req.body) 29 | .then(problem => res.send(problem)) 30 | .catch(next); 31 | }); 32 | 33 | router.delete('/:problemId', (req, res, next) => { 34 | req.problem.destroy() 35 | .then(() => res.sendStatus(204)) 36 | .catch(next); 37 | }); 38 | 39 | module.exports = router; 40 | -------------------------------------------------------------------------------- /server/problems.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const {expect} = require('chai'); 3 | const Problem = require('APP/db/models/problem'); 4 | const User = require('APP/db/models/user'); 5 | const app = require('./start'); 6 | 7 | describe('problems router', () => { 8 | const bill = {name: 'Bill', email: 'bill@test.com', password: '1234'}; 9 | let createdProblem; 10 | before(() => 11 | User.create(bill) 12 | ); 13 | 14 | describe('POST /api/problems', () => { 15 | const testProblem = {name: 'String Search', difficulty: 'easy'}; 16 | const agent = request.agent(app); 17 | before(() => agent 18 | .post('/api/auth/login') 19 | .send({username: bill.email, password: bill.password}) 20 | ); 21 | it('creates a new problem', () => agent 22 | .post('/api/problems') 23 | .send(testProblem) 24 | .expect(201) 25 | .then(res => { 26 | createdProblem = res.body; 27 | }) 28 | ); 29 | }); 30 | 31 | describe('GET /api/problems/:problemId', () => { 32 | it('gets problem by id', () => 33 | request(app) 34 | .get(`/api/problems/${createdProblem.id}`) 35 | .expect(200) 36 | .then(res => { 37 | expect(res.body.name).to.equal('String Search'); 38 | }) 39 | ); 40 | }); 41 | 42 | describe('PUT /api/problems/:problemId', () => { 43 | it('updates problem', () => 44 | request(app) 45 | .put(`/api/problems/${createdProblem.id}`) 46 | .send({name: 'String Permutations', difficulty: 'hard'}) 47 | .expect(200) 48 | .then(res => { 49 | expect(res.body.name).to.equal('String Permutations') 50 | }) 51 | ); 52 | }); 53 | 54 | describe('DELETE /api/problems/:problemId', () => { 55 | it('deleted given problem', () => 56 | request(app) 57 | .delete(`/api/problems/${createdProblem.id}`) 58 | .expect(204) 59 | .then(() => Problem.findById(createdProblem.id)) 60 | .then(p => { 61 | expect(p).to.be.null; 62 | }) 63 | ); 64 | }); 65 | }); 66 | 67 | -------------------------------------------------------------------------------- /server/redux/reducers/interview.js: -------------------------------------------------------------------------------- 1 | 2 | // Required 3 | const Immutable = require('immutable'); 4 | 5 | /* ----------------- ACTIONS ------------------ */ 6 | const ADD_ROOM = 'ADD_ROOM'; 7 | 8 | // Editor 9 | const SET_TEXT = 'SET_TEXT'; 10 | const SET_OPTIONS = 'SET_OPTIONS'; 11 | const SET_RANGE = 'SET_RANGE' 12 | const SET_RANGE_HISTORY = 'SET_RANGE_HISTORY'; 13 | const REMOVE_USER = 'REMOVE_USER'; 14 | 15 | // Whiteboard 16 | const REQUEST_HISTORY = 'REQUEST_HISTORY'; 17 | const SET_COORDINATES = 'SET_COORDINATES'; 18 | 19 | /* ------------ ACTION CREATORS ------------------ */ 20 | const addRoom = room => ({ 21 | type: ADD_ROOM, 22 | room 23 | }); 24 | 25 | // Editor 26 | const setText = text => ( 27 | { 28 | type: SET_TEXT, 29 | text 30 | } 31 | ); 32 | 33 | const setOptions = options => ({ 34 | type: SET_OPTIONS, 35 | options 36 | }); 37 | 38 | const setRangeHistory = ranges => ({ 39 | type: SET_RANGE_HISTORY, 40 | ranges 41 | }); 42 | 43 | const removeUser = userID => ({ 44 | type: REMOVE_USER, 45 | userID 46 | }); 47 | 48 | // Whiteboard 49 | const requestHistory = drawingHistory => { 50 | return { 51 | type: REQUEST_HISTORY, 52 | drawingHistory 53 | }; 54 | }; 55 | 56 | const setCoordinates = (lastPx, currentPx, color) => { 57 | return { 58 | type: SET_COORDINATES, 59 | lastDraw: { lastPx, currentPx, color } 60 | }; 61 | }; 62 | 63 | /* ------------ REDUCER ------------------ */ 64 | 65 | const defaultRange = Immutable.fromJS( 66 | { 67 | default: { 68 | start: { 69 | column: 0, 70 | row: 0 71 | }, 72 | end: { 73 | column: 0, 74 | row: 0 75 | } 76 | } 77 | } 78 | ); 79 | 80 | const defaultRoom = Immutable.fromJS( 81 | { 82 | editor: { 83 | text: `const CodeHuddle = 'built with <3';`, 84 | options: { 85 | linting: true, 86 | showGutter: true, 87 | textSize: false, 88 | theme: false 89 | }, 90 | ranges: defaultRange 91 | }, 92 | whiteboard: { 93 | drawingHistory: [] 94 | } 95 | } 96 | ); 97 | 98 | const initialInterviewData = Immutable.fromJS({ 99 | squidward: defaultRoom 100 | }); 101 | 102 | function reducer (interviewData = initialInterviewData, action) { 103 | let newInterviewData = interviewData; 104 | 105 | switch (action.type) { 106 | 107 | case ADD_ROOM: 108 | newInterviewData = newInterviewData.setIn([action.room], defaultRoom); 109 | break; 110 | 111 | case SET_TEXT: 112 | newInterviewData = newInterviewData.setIn([action.room, 'editor', 'text'], action.text); 113 | break; 114 | 115 | case SET_COORDINATES: 116 | newInterviewData = newInterviewData.updateIn( 117 | [action.room, 'whiteboard', 'drawingHistory'], 118 | drawingHistory => drawingHistory.push(action.lastDraw) 119 | ); 120 | break; 121 | 122 | case SET_OPTIONS: 123 | newInterviewData = newInterviewData.mergeIn([action.room, 'editor', 'options'], action.options); 124 | break; 125 | 126 | case SET_RANGE: 127 | const newRange = {}; 128 | newRange[action.id] = action.range; 129 | newInterviewData = newInterviewData.mergeIn([action.room, 'editor', 'ranges'], newRange); 130 | break; 131 | 132 | case REMOVE_USER: 133 | newInterviewData = newInterviewData.deleteIn([action.room, 'editor', 'ranges', action.userID]) 134 | break; 135 | 136 | default: return interviewData; 137 | 138 | } 139 | 140 | return newInterviewData; 141 | } 142 | 143 | /* ------------ DISPATCHERS ------------------ */ 144 | 145 | module.exports = { 146 | addRoom, 147 | setText, 148 | setOptions, 149 | setRangeHistory, 150 | removeUser, 151 | setCoordinates, 152 | requestHistory, 153 | reducer 154 | }; 155 | -------------------------------------------------------------------------------- /server/redux/store.js: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | const { createStore, combineReducers, applyMiddleware } = require('redux'); 4 | 5 | // Required files 6 | const interview = require('./reducers/interview').reducer; 7 | 8 | // Create the root reducer 9 | const rootReducer = combineReducers({ interview }); 10 | 11 | // Create the store with middleware 12 | const store = createStore(rootReducer); 13 | 14 | module.exports = { 15 | store 16 | }; 17 | -------------------------------------------------------------------------------- /server/sockets.js: -------------------------------------------------------------------------------- 1 | 2 | // Required libraries 3 | const chalk = require('chalk'); 4 | 5 | // Required files 6 | const { addRoom, setText, requestHistory, setOptions, removeUser, setRangeHistory } = require('./redux/reducers/interview'); 7 | const { store } = require('./redux/store'); 8 | 9 | // Util chalk logger for backend socket messages 10 | function socketLog (socketId, message) { 11 | console.log( 12 | chalk.bgYellow.white(` `), 13 | chalk.dim(`[Socket] client ID: ${socketId}`), 14 | chalk.yellow(message) 15 | ); 16 | } 17 | 18 | // This establishes the publish and subscribe function for the specific socket instance 19 | const socketPubSub = io => { 20 | io.on('connection', (socket) => { 21 | socketLog(socket.id, `is now connected`); 22 | 23 | let room; 24 | 25 | // Socket just connected, and wants to join a room. Grab the room information and send back to the user. 26 | socket.on('wantToJoinRoom', (roomName) => { 27 | // TODO: put into thunk action creator 28 | socketLog(socket.id, `has joined room: ${roomName}`); 29 | room = roomName; 30 | socket.join(room); 31 | 32 | let initialInterViewData = store.getState().interview; 33 | 34 | // If room doesn't exist, send out an action to create it with default text 35 | const haveRoomAlready = (initialInterViewData.keySeq().toArray().some(roomKey => { 36 | return roomKey === room; 37 | })); 38 | 39 | if (!haveRoomAlready) { 40 | store.dispatch(addRoom(room)); 41 | initialInterViewData = store.getState().interview; // Reset with updated data 42 | } 43 | 44 | const roomData = initialInterViewData.get(room).toJS(); 45 | 46 | // Create an action for the socket to emit to the requesting client 47 | const sendTextHistory = setText(roomData.editor.text); 48 | const sendTextOptions = setOptions(roomData.editor.options); 49 | const sendMarkerHistory = setRangeHistory(roomData.editor.ranges); 50 | const sendWhiteboardHistory = requestHistory(roomData.whiteboard.drawingHistory); 51 | 52 | socket.emit('clientStoreAction', sendTextHistory); 53 | socket.emit('clientStoreAction', sendTextOptions); 54 | socket.emit('clientStoreAction', sendMarkerHistory); 55 | socket.emit('clientStoreAction', sendWhiteboardHistory); 56 | }); 57 | 58 | // Socket sends out a client-side store action 59 | socket.on('clientStoreAction', (action) => { // When an action is received, send it out. This acts like a reducer. 60 | action.room = room; // Set room that the socket is in 61 | 62 | const acceptableActionTypes = new Set( 63 | ['SET_TEXT', 'SET_COORDINATES', 'SET_OPTIONS', 'SET_RANGE'] 64 | ); 65 | 66 | if (acceptableActionTypes.has(action.type)) { 67 | store.dispatch(action); 68 | } 69 | 70 | action.meta.remote = false; // Remove the remote true to prevent continuous back and forth. 71 | socket.broadcast.to(room).emit('clientStoreAction', action); // Broadcast out to everyone but the sender. 72 | }); 73 | 74 | socket.on('disconnect', () => { 75 | const removeUserHistory = removeUser(socket.id); 76 | removeUserHistory.room = room; 77 | store.dispatch(removeUserHistory); 78 | 79 | const interviewData = store.getState().interview.get(room); 80 | if (interviewData) { 81 | const roomData = interviewData.toJS(); 82 | const sendMarkerHistory = setRangeHistory(roomData.editor.ranges); 83 | socket.broadcast.to(room).emit('clientStoreAction', sendMarkerHistory); 84 | } 85 | 86 | socketLog(socket.id, `has disconnected`); 87 | }) 88 | }); 89 | }; 90 | 91 | module.exports = { 92 | socketPubSub 93 | }; 94 | -------------------------------------------------------------------------------- /server/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const express = require('express'); 4 | const bodyParser = require('body-parser'); 5 | const {resolve} = require('path'); 6 | const passport = require('passport'); 7 | const PrettyError = require('pretty-error'); 8 | const socketio = require('socket.io'); 9 | const { socketPubSub } = require('./sockets'); 10 | 11 | // Bones has a symlink from node_modules/APP to the root of the app. 12 | // That means that we can require paths relative to the app root by 13 | // saying require('APP/whatever'). 14 | // 15 | // This next line requires our root index.js: 16 | const pkg = require('APP'); 17 | 18 | const app = express(); 19 | 20 | if (!pkg.isProduction && !pkg.isTesting) { 21 | // Logging middleware (dev only) 22 | app.use(require('volleyball')); 23 | } 24 | 25 | // Pretty error prints errors all pretty. 26 | const prettyError = new PrettyError(); 27 | 28 | // Skip events.js and http.js and similar core node files. 29 | prettyError.skipNodeFiles(); 30 | 31 | // Skip all the trace lines about express' core and sub-modules. 32 | prettyError.skipPackage('express'); 33 | 34 | module.exports = app 35 | // We'll store the whole session in a cookie 36 | .use(require('cookie-session')({ 37 | name: 'session', 38 | keys: [process.env.SESSION_SECRET || 'an insecure secret key'] 39 | })) 40 | 41 | // Body parsing middleware 42 | .use(bodyParser.urlencoded({ extended: true })) 43 | .use(bodyParser.json()) 44 | 45 | // Authentication middleware 46 | .use(passport.initialize()) 47 | .use(passport.session()) 48 | 49 | // Serve static files from ../public 50 | 51 | .use(express.static(resolve(__dirname, '..', 'public'))) 52 | // ../node_modules/ 53 | // .use(express.static(resolve(__dirname, '..', 'node_modules'))) 54 | 55 | // Serve our api 56 | .use('/api', require('./api')) 57 | 58 | // Send index.html for anything else. 59 | .get('/*', (_, res) => res.sendFile(resolve(__dirname, '..', 'public', 'index.html'))) 60 | 61 | .use((err, req, res, next) => { 62 | console.log(prettyError.render(err)); 63 | res.status(500).send(err); 64 | next(); 65 | }); 66 | 67 | if (module === require.main) { 68 | // Start listening only if we're the main module. 69 | // 70 | // https://nodejs.org/api/modules.html#modules_accessing_the_main_module 71 | const server = app.listen( 72 | process.env.PORT || 1337, 73 | () => { 74 | console.log(`--- Started HTTP Server for ${pkg.name} ---`); 75 | console.log(`Listening on ${JSON.stringify(server.address())}`); 76 | 77 | // Socket initialization 78 | const io = socketio(server); 79 | socketPubSub(io); // Subscribe and emit setup 80 | } 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /server/users.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const db = require('APP/db'); 3 | const User = db.model('users'); 4 | const Organization = db.model('organizations'); 5 | const {mustBeLoggedIn, forbidden} = require('./auth.filters'); 6 | 7 | router.get('/', forbidden('only admins can list users'), (req, res, next) => { 8 | User.findAll() 9 | .then(users => res.json(users)) 10 | .catch(next); 11 | }); 12 | 13 | router.post('/', (req, res, next) => { 14 | User.create(req.body) 15 | .then(user => res.status(201).json(user)) 16 | .catch(next); 17 | }); 18 | 19 | router.param('userId', (req, res, next, id) => { 20 | User.findById(id, {include: [Organization]}) 21 | .then(user => { 22 | if (!user) res.send(404); 23 | else { 24 | req.foundUser = user; 25 | next(); 26 | } 27 | }) 28 | .catch(next); 29 | }); 30 | 31 | router.get('/:userId', (req, res, next) => { 32 | res.send(req.foundUser); 33 | }); 34 | 35 | router.put('/:userId', (req, res, next) => { 36 | req.foundUser.update(req.body) 37 | .then(user => res.send(user)) 38 | .catch(next); 39 | }); 40 | 41 | router.delete('/:userId', (req, res, next) => { 42 | req.foundUser.destroy() 43 | .then(() => res.sendStatus(204)) 44 | .catch(next); 45 | }); 46 | 47 | router.get('/:userId/problems', (req, res, next) => { 48 | req.foundUser.getProblems() 49 | .then(problems => res.send(problems)) 50 | .catch(next); 51 | }); 52 | 53 | router.get('/:userId/interviews', (req, res, next) => { 54 | req.user.getInterviews() 55 | .then(interviews => res.send(interviews)) 56 | .catch(next); 57 | }); 58 | 59 | module.exports = router; 60 | -------------------------------------------------------------------------------- /server/users.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const {expect} = require('chai'); 3 | const User = require('APP/db/models/user'); 4 | const Problem = require('APP/db/models/problem'); 5 | const app = require('./start'); 6 | 7 | describe('users router', () => { 8 | let userWithProblems; 9 | before(() => { 10 | let problems = [ 11 | {name: 'Subset Sum', difficulty: 'hard'}, 12 | {name: 'Intersection of Array', difficulty: 'medium'} 13 | ]; 14 | return User.create({ 15 | name: 'Dan', 16 | email: 'dan@test.com', 17 | password: '1234', 18 | company_name: 'Facebook', 19 | problems: problems 20 | }, { 21 | include: [Problem] 22 | }) 23 | .then(user => { 24 | userWithProblems = user; 25 | }); 26 | }); 27 | 28 | const alice = {name: 'Alice', email: 'alice@test.org', password: '12345', company_name: 'Google'}; 29 | 30 | let createdUser; 31 | 32 | describe('POST /api/users', () => { 33 | it('creates a new user', () => 34 | request(app) 35 | .post('/api/users') 36 | .send(alice) 37 | .expect(201) 38 | .then(res => { 39 | createdUser = res.body 40 | }) 41 | ); 42 | }); 43 | 44 | describe('GET /api/users/:userId', () => { 45 | it('returns user by ID', () => 46 | request(app) 47 | .get(`/api/users/${userWithProblems.id}`) 48 | .expect(200) 49 | .then(res => { 50 | expect(res.body.name).to.equal(userWithProblems.name) 51 | }) 52 | ); 53 | }); 54 | 55 | describe('PUT /api/users/:userId', () => { 56 | it('updates user', () => 57 | request(app) 58 | .put(`/api/users/${createdUser.id}`) 59 | .send({name: 'Jane'}) 60 | .expect(200) 61 | .then(res => expect(res.body.name).to.equal('Jane')) 62 | ); 63 | }); 64 | 65 | describe('DELETE /api/users/:userId', () => { 66 | it('deletes user', () => 67 | request(app) 68 | .delete(`/api/users/${createdUser.id}`) 69 | .expect(204) 70 | ); 71 | }); 72 | 73 | describe('GET /api/users/:userId/problems', () => { 74 | it('get all problems for given user', () => 75 | request(app) 76 | .get(`/api/users/${userWithProblems.id}/problems`) 77 | .expect(200) 78 | .then(res => { 79 | expect(res.body).to.have.lengthOf(2); 80 | }) 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | function slugify(organization) { 2 | const name = organization.name.toLowerCase(); 3 | const slugified = name.replace(/\s+/g, '-') // Replace spaces with - 4 | .replace(/[^\w\-]+/g, '') // Remove all non-word chars 5 | .replace(/\-\-+/g, '-') // Replace multiple - with single - 6 | .replace(/^-+/, '') // Trim - from start of text 7 | .replace(/-+$/, ''); // Trim - from end of text 8 | organization.slug = slugified; 9 | } 10 | 11 | module.exports = {slugify}; 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | module.exports = { 6 | entry: './app/main.jsx', 7 | output: { 8 | path: __dirname, 9 | filename: './public/bundle.js' 10 | }, 11 | context: __dirname, 12 | devtool: 'source-map', 13 | resolve: { 14 | extensions: ['', '.js', '.jsx'] 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /jsx?$/, 20 | exclude: /(node_modules|bower_components)/, 21 | loader: 'babel', 22 | query: { 23 | presets: ['react', 'es2015', 'stage-2'] 24 | } 25 | } 26 | ] 27 | } 28 | }; 29 | --------------------------------------------------------------------------------