├── .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 | 
2 | ---
3 | 
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 | 
53 | #### Interviewer dashboard, displays all the interviews
54 | 
55 | #### Interview planning, allows the interviewer to edit the interview details and problems
56 | 
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 |
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 |
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 |
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 | Logout
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 |
41 |
47 |
48 |
49 |
89 |
90 |
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 |
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 |
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 |
14 | );
15 | };
--------------------------------------------------------------------------------
/app/components/splash/Splash-Nav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default () => {
4 | return (
5 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------