├── .gitignore ├── LICENSE ├── README.md ├── client ├── .gitignore ├── README.md ├── config │ └── config.json ├── package.json ├── public │ ├── index.html │ └── vendor.json ├── src │ ├── actions │ │ ├── actionTypes.js │ │ ├── ajaxStatusActions.js │ │ ├── courseActions.js │ │ ├── departmentActions.js │ │ ├── enrollmentActions.js │ │ ├── instructorActions.js │ │ └── studentActions.js │ ├── components │ │ ├── AboutPage.js │ │ ├── AppContainer.js │ │ ├── CoursesPage.js │ │ ├── DepartmentsPage.js │ │ ├── HomePage.js │ │ ├── InstructorsPage.js │ │ ├── Navigation.js │ │ ├── NotFoundPage.js │ │ ├── StudentsPage.js │ │ ├── common │ │ │ ├── CheckBox.js │ │ │ ├── DateTimePicker.js │ │ │ ├── DisplayRow.js │ │ │ ├── NumbertInput.js │ │ │ ├── SelectInput.js │ │ │ └── TextInput.js │ │ ├── courses │ │ │ ├── CourseDelete.js │ │ │ ├── CourseDetails.js │ │ │ ├── CourseForm.js │ │ │ ├── CourseSave.js │ │ │ ├── CoursesFilter.js │ │ │ ├── CoursesList.js │ │ │ └── CoursesRow.js │ │ ├── department │ │ │ ├── DepartmentDelete.js │ │ │ ├── DepartmentDetails.js │ │ │ ├── DepartmentForm.js │ │ │ ├── DepartmentRow.js │ │ │ ├── DepartmentSave.js │ │ │ └── DepartmentsList.js │ │ ├── instructor │ │ │ ├── InstructorCourseRow.js │ │ │ ├── InstructorCoursesList.js │ │ │ ├── InstructorDelete.js │ │ │ ├── InstructorDetails.js │ │ │ ├── InstructorForm.js │ │ │ ├── InstructorList.js │ │ │ ├── InstructorRow.js │ │ │ ├── InstructorSave.js │ │ │ ├── InstructorStudentRow.js │ │ │ └── InstructorStudentsList.js │ │ └── student │ │ │ ├── StudentDelete.js │ │ │ ├── StudentDetails.js │ │ │ ├── StudentEnrollmentsList.js │ │ │ ├── StudentEnrollmentsRow.js │ │ │ ├── StudentForm.js │ │ │ ├── StudentRow.js │ │ │ ├── StudentSave.js │ │ │ ├── StudentSearch.js │ │ │ ├── StudentStatisticsList.js │ │ │ ├── StudentStatisticsRow.js │ │ │ └── StudentsList.js │ ├── formatters │ │ ├── currencyFormatter.js │ │ ├── dateFormatter.js │ │ ├── entityFromatter.js │ │ └── personFormatter.js │ ├── helpers │ │ ├── clientConfig.js │ │ ├── httpHelper.js │ │ └── uiHelper.js │ ├── index.js │ ├── reducers │ │ ├── ajaxStatusReducer.js │ │ ├── courseReducer.js │ │ ├── departmentReducer.js │ │ ├── enrollmentReducer.js │ │ ├── index.js │ │ ├── initialState.js │ │ ├── instructorReducer.js │ │ └── studentReducer.js │ ├── routes.js │ ├── services │ │ ├── courseService.js │ │ ├── departmentService.js │ │ ├── enrollmentService.js │ │ ├── instructorService.js │ │ └── studentService.js │ └── store │ │ ├── configureStore.dev.js │ │ ├── configureStore.js │ │ └── configureStore.prod.js ├── styles │ └── app.css └── tasks │ └── copyAssets.js ├── docs └── setup.md ├── package.json └── server ├── .gitignore ├── data ├── config │ └── default.json ├── emails │ ├── activation │ │ ├── html.hbs │ │ └── subject.hbs │ └── password_reset │ │ ├── html.hbs │ │ └── subject.hbs ├── seed │ └── seedData.json └── text │ └── textValues.json ├── package.json ├── src ├── appError.ts ├── auth │ ├── authController.ts │ ├── authHelper.ts │ ├── authInit.ts │ └── providers │ │ ├── facebookAuth.ts │ │ └── googleAuth.ts ├── config.ts ├── controllers │ ├── _controllerHelper.ts │ ├── courseController.ts │ ├── departmentController.ts │ ├── enrollmentController.ts │ ├── homeController.ts │ ├── instructorController.ts │ └── studentController.ts ├── database │ ├── database.ts │ ├── models │ │ ├── _modelHelper.ts │ │ ├── course.ts │ │ ├── department.ts │ │ ├── enrollment.ts │ │ ├── index.ts │ │ ├── instructor.ts │ │ ├── officeAssignment.ts │ │ ├── student.ts │ │ └── user.ts │ └── seeders │ │ └── seederDefault.ts ├── helpers │ ├── emailHelper.ts │ ├── errorHelper.ts │ ├── pathHelper.ts │ └── textValueHelper.ts ├── logger.ts ├── repositories │ ├── _repositoryHelper.ts │ ├── courseRepository.ts │ ├── departmentRepository.ts │ ├── enrollmentRepository.ts │ ├── instructorRepository.ts │ ├── officeAssignmentRepository.ts │ ├── studentRepository.ts │ └── userRepository.ts ├── routes │ ├── routes.ts │ └── routesHelper.ts ├── server.ts ├── startServer.ts ├── tasks │ ├── syncDb.ts │ └── typingsSymLink.ts └── tests │ ├── _testHelper.ts │ ├── courseRepositoryTest.ts │ ├── departmentRepositoryTest.ts │ ├── enrollmentRepositoryTest.ts │ ├── instructorRepositoryTest.ts │ ├── officeAssignmentRepositoryTest.ts │ ├── studentRepositoryTest.ts │ └── userRepositoryTest.ts ├── tsconfig.json ├── tslint.json ├── typings.json ├── typings ├── app │ ├── custom.d.ts │ └── models.d.ts └── index.d.ts └── views ├── layouts └── auth.hbs ├── login.hbs ├── partials └── loginRedirectTip.hbs ├── password-forgot.hbs ├── password-reset.hbs └── signup.hbs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | client/public/vendor 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yegor Sytnyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Contoso Express 2 | 3 | Full-stack JavaScript sample app. Remake of famous Microsoft Contoso University tutorial. This project provides many basic infrastructure parts, required in modern Web Application such as: 4 | 5 | * project structure 6 | * data access 7 | * authentication 8 | * error handling 9 | * logging 10 | * configuration 11 | * client side as SPA (Single Page App) 12 | * unit testing 13 | * etc 14 | 15 | ## Missing tutorial 16 | 17 | A good thing about the node is that you have flexibility to build your own stack, the bad thing is that you have no other choise. There are no frameworks which will get you all you need, so you have to build our own foundation. 18 | 19 | This project is the missing manual (sample) which shows one of the ways to build common infrastructure. That is good starting point if you are getting familiar with Node, as you do not need to do everything yourself. 20 | 21 | ## Get started 22 | 23 | See instructions here: [Getting Started](https://github.com/yegor-sytnyk/contoso-express/wiki/Getting-started) 24 | 25 | ## Technologies 26 | 27 | * Language: TypeScript (alt JS) 28 | * DB: PostgreSQL (alt MongoDb) 29 | * Data Access: Sequelize 30 | * ExpressJS 31 | * Logging: winston 32 | * Auth: Passport 33 | * Validation: Joi 34 | * Build: npm scripts 35 | * Client render: React (alt Vue.js) 36 | * Client data: Redux 37 | * Client build: webpack 38 | 39 | ** ALT - means alternative implementation is planned. For some areas it is hard to choose the best option, so would be nice to have alternative sample. -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | public/static/vendor 17 | -------------------------------------------------------------------------------- /client/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "isDevLocal": true, 3 | "proxy": "http://localhost:3500", 4 | "format": { 5 | "date": "MM/DD/YYYY", 6 | "year": "YYYY", 7 | "currencySymbol": "$" 8 | } 9 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "react-scripts": "0.6.1" 7 | }, 8 | "scripts": { 9 | "start": "react-scripts start", 10 | "build": "react-scripts build", 11 | "test": "react-scripts test --env=jsdom", 12 | "eject": "react-scripts eject", 13 | "copy-assets": "node tasks/copyAssets.js" 14 | }, 15 | "dependencies": { 16 | "bluebird": "^3.4.0", 17 | "bootstrap": "^3.3.6", 18 | "copy": "^0.3.0", 19 | "font-awesome": "^4.6.3", 20 | "fs-extra": "^0.30.0", 21 | "jquery": "^3.0.0", 22 | "json-loader": "^0.5.4", 23 | "lodash": "^4.13.1", 24 | "moment": "^2.14.1", 25 | "react": "^15.3.2", 26 | "react-bootstrap": "^0.30.5", 27 | "react-bootstrap-datetimepicker": "0.0.22", 28 | "react-dom": "^15.3.2", 29 | "react-redux": "^4.4.5", 30 | "react-router": "^2.4.1", 31 | "react-router-bootstrap": "^0.23.0", 32 | "redux": "^3.5.2", 33 | "redux-immutable-state-invariant": "^1.2.3", 34 | "redux-thunk": "^2.1.0", 35 | "toastr": "^2.1.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | Contoso Express 20 | 21 | 22 |
23 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /client/public/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "staticResources": [ 3 | {"from": "node_modules/font-awesome/css/font-awesome.min.css", "to": "vendor/font-awesome/css/font-awesome.min.css"}, 4 | {"from": "node_modules/font-awesome/fonts", "to": "vendor/font-awesome/fonts"}, 5 | {"from": "node_modules/bootstrap/dist/css/bootstrap.min.css", "to": "vendor/bootstrap/css/bootstrap.min.css"}, 6 | {"from": "node_modules/bootstrap/dist/css/bootstrap.min.css.map", "to": "vendor/bootstrap/css/bootstrap.min.css.map"}, 7 | {"from": "node_modules/bootstrap/dist/fonts", "to": "vendor/bootstrap/fonts"}, 8 | {"from": "node_modules/toastr/build/toastr.min.css", "to": "vendor/toastr/toastr.min.css"} 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /client/src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const BEGIN_AJAX_CALL = 'BEGIN_AJAX_CALL'; 2 | export const AJAX_CALL_ERROR = 'AJAX_CALL_ERROR'; 3 | 4 | export const LOAD_STUDENTS_STATISTICS_SUCCESS = 'LOAD_STUDENTS_STATISTICS_SUCCESS'; 5 | export const LOAD_STUDENTS_SUCCESS = 'LOAD_STUDENTS_SUCCESS'; 6 | export const COUNT_STUDENTS_SUCCCESS = 'COUNT_STUDENTS_SUCCCESS'; 7 | export const LOAD_STUDENT_SUCCESS = 'LOAD_STUDENT_SUCCESS'; 8 | export const CREATE_STUDENT_SUCCESS = 'CREATE_STUDENT_SUCCESS'; 9 | export const UPDATE_STUDENT_SUCCESS = 'UPDATE_STUDENT_SUCCESS'; 10 | export const DELETE_STUDENT_SUCCESS = 'DELETE_STUDENT_SUCCESS'; 11 | 12 | export const LOAD_ENROLLMENTS_SUCCESS = 'LOAD_ENROLLMENTS_SUCCESS'; 13 | 14 | export const LOAD_COURSES_SUCCESS = 'LOAD_COURSES_SUCCESS'; 15 | export const LOAD_COURSE_SUCCESS = 'LOAD_COURSE_SUCCESS'; 16 | export const UPDATE_COURSE_SUCCESS = 'UPDATE_COURSE_SUCCESS'; 17 | export const CREATE_COURSE_SUCCESS = 'CREATE_COURSE_SUCCESS'; 18 | export const DELETE_COURSE_SUCCESS = 'DELETE_COURSE_SUCCESS'; 19 | 20 | export const LOAD_INSTRUCTORS_SUCCESS = 'LOAD_INSTRUCTORS_SUCCESS'; 21 | export const LOAD_INSTRUCTOR_SUCCESS = 'LOAD_INSTRUCTOR_SUCCESS'; 22 | export const CREATE_INSTRUCTOR_SUCCESS = 'CREATE_INSTRUCTOR_SUCCESS'; 23 | export const UPDATE_INSTRUCTOR_SUCCESS = 'UPDATE_INSTRUCTOR_SUCCESS'; 24 | export const DELETE_INSTRUCTOR_SUCCESS = 'DELETE_INSTRUCTOR_SUCCESS'; 25 | 26 | export const LOAD_DEPARTMENTS_SUCCESS = 'LOAD_DEPARTMENTS_SUCCESS'; 27 | export const LOAD_DEPARTMENT_SUCCESS = 'LOAD_DEPARTMENT_SUCCESS'; 28 | export const CREATE_DEPARTMENT_SUCCESS = 'CREATE_DEPARTMENT_SUCCESS'; 29 | export const UPDATE_DEPARTMENT_SUCCESS = 'UPDATE_DEPARTMENT_SUCCESS'; 30 | export const DELETE_DEPARTMENT_SUCCESS = 'DELETE_DEPARTMENT_SUCCESS'; 31 | 32 | -------------------------------------------------------------------------------- /client/src/actions/ajaxStatusActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | 3 | export function beginAjaxCall() { 4 | return {type: types.BEGIN_AJAX_CALL}; 5 | } 6 | 7 | export function ajaxCallError() { 8 | return {type: types.AJAX_CALL_ERROR}; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /client/src/actions/courseActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | import {beginAjaxCall} from './ajaxStatusActions'; 3 | import courseService from '../services/courseService'; 4 | import _ from 'lodash'; 5 | 6 | export function loadCoursesSuccess(courses) { 7 | return {type: types.LOAD_COURSES_SUCCESS, courses}; 8 | } 9 | 10 | export function loadCourseSuccess(course) { 11 | return {type: types.LOAD_COURSE_SUCCESS, course}; 12 | } 13 | 14 | export function createCourseSuccess(course) { 15 | return {type: types.CREATE_COURSE_SUCCESS, course}; 16 | } 17 | 18 | export function updateCourseSuccess(course) { 19 | return {type: types.UPDATE_COURSE_SUCCESS, course}; 20 | } 21 | 22 | export function deleteCourseSuccess(id) { 23 | return {type: types.DELETE_COURSE_SUCCESS, id}; 24 | } 25 | 26 | export function loadCourses(departmentId) { 27 | return dispatch => { 28 | dispatch(beginAjaxCall()); 29 | 30 | return courseService.getCourses(departmentId) 31 | .then(courses => { 32 | dispatch(loadCoursesSuccess(courses)); 33 | }).catch(error => { 34 | throw(error); 35 | }); 36 | }; 37 | } 38 | 39 | export function loadCourse(id) { 40 | return dispatch => { 41 | dispatch(beginAjaxCall()); 42 | 43 | let action = Promise.resolve(null); 44 | 45 | if (_.isNumber(id)) { 46 | action = courseService.getCourse(id); 47 | } 48 | 49 | return action 50 | .then(course => { 51 | if (!course) { 52 | course = { 53 | number: '', 54 | title: '', 55 | credits: '', 56 | departmentId: '', 57 | department: {name: ''} 58 | }; 59 | } 60 | 61 | dispatch(loadCourseSuccess(course)); 62 | }) 63 | .catch(error => { 64 | throw(error); 65 | }); 66 | }; 67 | } 68 | 69 | export function saveCourse(course) { 70 | return dispatch => { 71 | dispatch(beginAjaxCall()); 72 | 73 | return courseService.saveCourse(course) 74 | .then((data) => { 75 | return course.id ? dispatch(updateCourseSuccess(data)) : dispatch(createCourseSuccess(data)); 76 | }).catch(error => { 77 | throw(error); 78 | }); 79 | }; 80 | } 81 | 82 | export function deleteCourse(id) { 83 | return dispatch => { 84 | dispatch(beginAjaxCall()); 85 | 86 | return courseService.deleteCourse(id) 87 | .then(() => { 88 | dispatch(deleteCourseSuccess(id)); 89 | }).catch(error => { 90 | throw(error); 91 | }); 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /client/src/actions/departmentActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | import _ from 'lodash'; 3 | import Promise from 'bluebird'; 4 | import {beginAjaxCall} from './ajaxStatusActions'; 5 | import departmentService from '../services/departmentService'; 6 | import dateFormatter from '../formatters/dateFormatter'; 7 | 8 | export function loadDepartmentsSuccess(departments) { 9 | return {type: types.LOAD_DEPARTMENTS_SUCCESS, departments}; 10 | } 11 | 12 | export function loadDepartmentSuccess(department) { 13 | return {type: types.LOAD_DEPARTMENT_SUCCESS, department}; 14 | } 15 | 16 | export function createDepartmentSuccess(department) { 17 | return {type: types.CREATE_DEPARTMENT_SUCCESS, department}; 18 | } 19 | 20 | export function updateDepartmentSuccess(department) { 21 | return {type: types.UPDATE_DEPARTMENT_SUCCESS, department}; 22 | } 23 | 24 | export function deleteDepartmentSuccess(id) { 25 | return {type: types.DELETE_DEPARTMENT_SUCCESS, id}; 26 | } 27 | 28 | export function loadDepartments() { 29 | return dispatch => { 30 | dispatch(beginAjaxCall()); 31 | 32 | return departmentService.getDepartments() 33 | .then(departments => { 34 | dispatch(loadDepartmentsSuccess(departments)); 35 | }).catch(error => { 36 | throw(error); 37 | }); 38 | }; 39 | } 40 | 41 | export function loadDepartment(id) { 42 | return dispatch => { 43 | dispatch(beginAjaxCall()); 44 | 45 | let action = Promise.resolve(null); 46 | 47 | if (_.isNumber(id)) { 48 | action = departmentService.getDepartment(id); 49 | } 50 | 51 | return action 52 | .then(department => { 53 | if (!department) { 54 | department = { 55 | name: '', 56 | budget: '', 57 | startDate: dateFormatter.currentDate(), 58 | instructorId: '', 59 | Instructor: {lastName: '', firstName: ''} 60 | }; 61 | } 62 | 63 | dispatch(loadDepartmentSuccess(department)); 64 | }) 65 | .catch(error => { 66 | throw(error); 67 | }); 68 | }; 69 | } 70 | 71 | export function saveDepartment(department) { 72 | return dispatch => { 73 | dispatch(beginAjaxCall()); 74 | 75 | return departmentService.saveDepartment(department) 76 | .then((dep) => { 77 | return department.id ? dispatch(updateDepartmentSuccess(dep)) : dispatch(createDepartmentSuccess(dep)); 78 | }).catch(error => { 79 | throw(error); 80 | }); 81 | }; 82 | } 83 | 84 | export function deleteDepartment(id) { 85 | return dispatch => { 86 | dispatch(beginAjaxCall()); 87 | 88 | return departmentService.deleteDepartment(id) 89 | .then(() => { 90 | dispatch(deleteDepartmentSuccess(id)); 91 | }).catch(error => { 92 | throw(error); 93 | }); 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /client/src/actions/enrollmentActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | import {beginAjaxCall} from './ajaxStatusActions'; 3 | import enrollmentService from '../services/enrollmentService'; 4 | import _ from 'lodash'; 5 | 6 | export function loadEnrollmentsSuccess(enrollments) { 7 | return {type: types.LOAD_ENROLLMENTS_SUCCESS, enrollments}; 8 | } 9 | 10 | export function loadEnrollments(courseId) { 11 | return dispatch => { 12 | dispatch(beginAjaxCall()); 13 | 14 | return enrollmentService.getEnrollments(courseId) 15 | .then(enrollments => { 16 | dispatch(loadEnrollmentsSuccess(enrollments)); 17 | }).catch(error => { 18 | throw(error); 19 | }); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /client/src/actions/instructorActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | import _ from 'lodash'; 3 | import {beginAjaxCall} from './ajaxStatusActions'; 4 | import instructorService from '../services/instructorService'; 5 | import dateFormatter from '../formatters/dateFormatter'; 6 | 7 | export function loadInstructorsSuccess(instructors) { 8 | return {type: types.LOAD_INSTRUCTORS_SUCCESS, instructors}; 9 | } 10 | 11 | export function loadInstructorSuccess(instructor) { 12 | return {type: types.LOAD_INSTRUCTOR_SUCCESS, instructor}; 13 | } 14 | 15 | export function createInstructorSuccess(instructor) { 16 | return {type: types.CREATE_INSTRUCTOR_SUCCESS, instructor}; 17 | } 18 | 19 | export function updateInstructorSuccess(instructor) { 20 | return {type: types.UPDATE_INSTRUCTOR_SUCCESS, instructor}; 21 | } 22 | 23 | export function deleteInstructorSuccess(id) { 24 | return {type: types.DELETE_INSTRUCTOR_SUCCESS, id}; 25 | } 26 | 27 | export function loadInstructors() { 28 | return dispatch => { 29 | dispatch(beginAjaxCall()); 30 | 31 | return instructorService.getInstructors() 32 | .then(instructors => { 33 | dispatch(loadInstructorsSuccess(instructors)); 34 | }).catch(error => { 35 | throw(error); 36 | }); 37 | }; 38 | } 39 | 40 | export function loadInstructor(id) { 41 | return dispatch => { 42 | dispatch(beginAjaxCall()); 43 | 44 | let action = Promise.resolve(null); 45 | 46 | if (_.isNumber(id)) { 47 | action = instructorService.getInstructor(id); 48 | } 49 | 50 | return action 51 | .then(instructor => { 52 | if (!instructor) { 53 | instructor = { 54 | firstName: '', 55 | lastName: '', 56 | hireDate: dateFormatter.currentDate(), 57 | officeAssignment: { 58 | location: '' 59 | }, 60 | courses: [] 61 | }; 62 | } else { 63 | if (!instructor.officeAssignment) { 64 | instructor.officeAssignment = { 65 | location: '' 66 | }; 67 | } 68 | } 69 | 70 | dispatch(loadInstructorSuccess(instructor)); 71 | }) 72 | .catch(error => { 73 | throw(error); 74 | }); 75 | }; 76 | } 77 | 78 | export function saveInstructor(instructor) { 79 | return dispatch => { 80 | dispatch(beginAjaxCall()); 81 | 82 | return instructorService.saveInstructor(instructor) 83 | .then((data) => { 84 | return instructor.id ? dispatch(updateInstructorSuccess(data)) : dispatch(createInstructorSuccess(data)); 85 | }).catch(error => { 86 | throw(error); 87 | }); 88 | }; 89 | } 90 | 91 | export function deleteInstructor(id) { 92 | return dispatch => { 93 | dispatch(beginAjaxCall()); 94 | 95 | return instructorService.deleteInstructor(id) 96 | .then(() => { 97 | dispatch(deleteInstructorSuccess(id)); 98 | }).catch(error => { 99 | throw(error); 100 | }); 101 | }; 102 | } -------------------------------------------------------------------------------- /client/src/actions/studentActions.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | import _ from 'lodash'; 3 | import {beginAjaxCall} from './ajaxStatusActions'; 4 | import studentService from '../services/studentService'; 5 | import dateFormatter from '../formatters/dateFormatter'; 6 | 7 | export function loadStudentsStatisticsSuccess(statistics) { 8 | return {type: types.LOAD_STUDENTS_STATISTICS_SUCCESS, statistics}; 9 | } 10 | 11 | export function loadStudentsSuccess(students) { 12 | return {type: types.LOAD_STUDENTS_SUCCESS, students}; 13 | } 14 | 15 | export function loadStudentSuccess(student) { 16 | return {type: types.LOAD_STUDENT_SUCCESS, student}; 17 | } 18 | 19 | export function countStudentsSuccess(count) { 20 | return {type: types.COUNT_STUDENTS_SUCCCESS, count}; 21 | } 22 | 23 | export function createStudentSuccess(student) { 24 | return {type: types.CREATE_STUDENT_SUCCESS, student}; 25 | } 26 | 27 | export function updateStudentSuccess(student) { 28 | return {type: types.UPDATE_STUDENT_SUCCESS, student}; 29 | } 30 | 31 | export function deleteStudentSuccess(id) { 32 | return {type: types.DELETE_STUDENT_SUCCESS, id}; 33 | } 34 | 35 | export function loadStudentsStatistics() { 36 | return dispatch => { 37 | dispatch(beginAjaxCall()); 38 | 39 | return studentService.getStudentsStatistics() 40 | .then(statistics => { 41 | dispatch(loadStudentsStatisticsSuccess(statistics)); 42 | }).catch(error => { 43 | throw(error); 44 | }); 45 | }; 46 | } 47 | 48 | export function loadStudents(search, sortOrder, pageNumber, pageSize) { 49 | return dispatch => { 50 | dispatch(beginAjaxCall()); 51 | 52 | return studentService.getStudents(search, sortOrder, pageNumber, pageSize) 53 | .then(data => { 54 | dispatch(loadStudentsSuccess(data.rows)); 55 | dispatch(countStudentsSuccess(data.count)); 56 | }).catch(error => { 57 | throw(error); 58 | }); 59 | }; 60 | } 61 | 62 | export function loadStudent(id) { 63 | return dispatch => { 64 | dispatch(beginAjaxCall()); 65 | 66 | let action = Promise.resolve(null); 67 | 68 | if (_.isNumber(id)) { 69 | action = studentService.getStudent(id); 70 | } 71 | 72 | return action 73 | .then(student => { 74 | if (!student) { 75 | student = { 76 | firstName: '', 77 | lastName: '', 78 | enrollmentDate: dateFormatter.currentDate() 79 | }; 80 | } 81 | 82 | dispatch(loadStudentSuccess(student)); 83 | }) 84 | .catch(error => { 85 | throw(error); 86 | }); 87 | }; 88 | } 89 | 90 | export function saveStudent(student) { 91 | return dispatch => { 92 | dispatch(beginAjaxCall()); 93 | 94 | return studentService.saveStudent(student) 95 | .then((stud) => { 96 | return student.id ? dispatch(updateStudentSuccess(stud)) : dispatch(createStudentSuccess(stud)); 97 | }).catch(error => { 98 | throw(error); 99 | }); 100 | }; 101 | } 102 | 103 | export function deleteStudent(id) { 104 | return dispatch => { 105 | dispatch(beginAjaxCall()); 106 | 107 | return studentService.deleteStudent(id) 108 | .then(() => { 109 | dispatch(deleteStudentSuccess(id)); 110 | }).catch(error => { 111 | throw(error); 112 | }); 113 | }; 114 | } -------------------------------------------------------------------------------- /client/src/components/AboutPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import StudentStatisticsList from './student/StudentStatisticsList'; 4 | import {loadStudentsStatistics} from '../actions/studentActions'; 5 | 6 | class AboutPage extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | 10 | this.state = { 11 | statistics: props.statistics 12 | }; 13 | } 14 | 15 | componentWillMount() { 16 | this.props.loadStudentsStatistics(); 17 | } 18 | 19 | render() { 20 | return ( 21 |
22 |

Student Body Statistics

23 | 24 |
25 | 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | AboutPage.propTypes = { 33 | statistics: React.PropTypes.array.isRequired 34 | }; 35 | 36 | function mapStateToProps(state) { 37 | return { 38 | statistics: state.student.statisticsList 39 | }; 40 | } 41 | 42 | function mapDispatchToProps(dispatch) { 43 | return { 44 | loadStudentsStatistics: () => loadStudentsStatistics()(dispatch) 45 | }; 46 | } 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(AboutPage); -------------------------------------------------------------------------------- /client/src/components/AppContainer.js: -------------------------------------------------------------------------------- 1 | // This component handles the App template used on every page. 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | import Navigation from './Navigation'; 5 | import dateFormatter from '../formatters/dateFormatter'; 6 | 7 | class App extends React.Component { 8 | render() { 9 | let date = dateFormatter.currentYear(); 10 | 11 | return ( 12 |
13 | 14 |
15 | {this.props.children} 16 | 17 |
18 |
19 | 20 |

© {date} - Contoso University

21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | static propTypes = { 28 | children: React.PropTypes.object.isRequired 29 | }; 30 | 31 | } 32 | 33 | 34 | function mapStateToProps(state, ownProps) { 35 | return { 36 | 37 | }; 38 | } 39 | 40 | export default connect(mapStateToProps)(App); -------------------------------------------------------------------------------- /client/src/components/DepartmentsPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import DepartmentsList from './department/DepartmentsList'; 5 | import DepartmentSave from './department/DepartmentSave'; 6 | import DepartmentDetails from './department/DepartmentDetails'; 7 | import DepartmentDelete from './department/DepartmentDelete'; 8 | import * as departmentActions from '../actions/departmentActions'; 9 | 10 | class DepartmentsPage extends React.Component { 11 | constructor(props, context) { 12 | super(props, context); 13 | 14 | this.state = { 15 | departments: props.departments, 16 | saveModalVisible: false, 17 | detailsModalVisible: false, 18 | confirmationVisible: false 19 | }; 20 | 21 | this.showSaveModal = this.showSaveModal.bind(this); 22 | this.closeSaveModal = this.closeSaveModal.bind(this); 23 | this.showDetailsModal = this.showDetailsModal.bind(this); 24 | this.closeDetailsModal = this.closeDetailsModal.bind(this); 25 | this.showConfirmationModal = this.showConfirmationModal.bind(this); 26 | this.closeConfirmationModal = this.closeConfirmationModal.bind(this); 27 | } 28 | 29 | showSaveModal(departmentId) { 30 | this.props.actions.loadDepartment(departmentId) 31 | .then(() => { 32 | this.setState({saveModalVisible: true}); 33 | }); 34 | } 35 | 36 | closeSaveModal() { 37 | this.setState({saveModalVisible: false}); 38 | } 39 | 40 | showDetailsModal(departmentId) { 41 | this.props.actions.loadDepartment(departmentId) 42 | .then(() => { 43 | this.setState({detailsModalVisible: true}); 44 | }); 45 | } 46 | 47 | closeDetailsModal() { 48 | this.setState({detailsModalVisible: false}); 49 | } 50 | 51 | showConfirmationModal(departmentId) { 52 | this.props.actions.loadDepartment(departmentId) 53 | .then(() => { 54 | this.setState({confirmationVisible: true}); 55 | }); 56 | } 57 | 58 | closeConfirmationModal() { 59 | this.setState({confirmationVisible: false}); 60 | } 61 | 62 | render() { 63 | return ( 64 |
65 |

Departments

66 | Create New 67 | 68 | 73 | 74 | 77 | 78 | 81 | 82 | 85 |
86 | ); 87 | } 88 | } 89 | 90 | DepartmentsPage.propTypes = { 91 | departments: React.PropTypes.array.isRequired, 92 | actions: React.PropTypes.object.isRequired 93 | }; 94 | 95 | function mapStateToProps(state) { 96 | return { 97 | departments: state.department.list 98 | }; 99 | } 100 | 101 | function mapDispatchToProps(dispatch) { 102 | return { 103 | actions: bindActionCreators(departmentActions, dispatch) 104 | }; 105 | } 106 | 107 | export default connect(mapStateToProps, mapDispatchToProps)(DepartmentsPage); -------------------------------------------------------------------------------- /client/src/components/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class HomePage extends React.Component { 4 | render() { 5 | return ( 6 |
7 |
8 |

Contoso University

9 |
10 | 11 |
12 |
13 |

Welcome to Contoso Express

14 |

15 | Contoso Express is a sample application that 16 | demonstrates best practices of modern JavaScript full-stack development. 17 |

18 |
19 | 20 |
21 |

Original Contoso

22 |

That is remake of famous in .NET world Contoso Express project.

23 |

Generally data base schema and functionality stays the same; UI is a bit updated as project demonstrates SPA techniques for client side.

24 |

25 | See original tutorial here » 26 |

27 |
28 | 29 |
30 |

Source

31 |

See the latest source code on GitHub.

32 |

33 | Check out source code on github » 34 |

35 |
36 |
37 |
38 | ); 39 | } 40 | } 41 | 42 | export default HomePage; -------------------------------------------------------------------------------- /client/src/components/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {IndexLink } from 'react-router'; 3 | import {Navbar, Nav, NavItem} from 'react-bootstrap'; 4 | import { LinkContainer, IndexLinkContainer } from 'react-router-bootstrap'; 5 | 6 | class Navigation extends React.Component { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | Contoso University 13 | 14 | 15 | 16 | 17 | 18 | 43 | 46 | 47 | 48 | ); 49 | } 50 | } 51 | 52 | export default Navigation; -------------------------------------------------------------------------------- /client/src/components/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NotFoundPage = () => { 4 | return ( 5 |
6 |

Not found

7 |

Sorry! The page you are looking for could not be found.

8 |
9 | ); 10 | }; 11 | 12 | export default NotFoundPage; -------------------------------------------------------------------------------- /client/src/components/common/CheckBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CheckBox = ({name, label, onChange, value}) => { 4 | 5 | return ( 6 |
7 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | CheckBox.propTypes = { 20 | name: React.PropTypes.number.isRequired, 21 | label: React.PropTypes.string.isRequired, 22 | onChange: React.PropTypes.func.isRequired, 23 | value: React.PropTypes.string, 24 | }; 25 | 26 | export default CheckBox; -------------------------------------------------------------------------------- /client/src/components/common/DateTimePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DateTimeField from 'react-bootstrap-datetimepicker'; 3 | import config from '../../helpers/clientConfig'; 4 | import dateFormatter from '../../formatters/dateFormatter'; 5 | 6 | const DateTimePicker = ({name, label, onChange, value, error}) => { 7 | let wrapperClass = 'form-group'; 8 | if (error && error.length > 0) { 9 | wrapperClass += ' has-error'; 10 | } 11 | 12 | let dateValue = null; 13 | 14 | if (dateFormatter.isValid(value)) { 15 | dateValue = dateFormatter.date(value); 16 | } 17 | 18 | return ( 19 |
20 | 21 |
22 | 29 | {error &&
{error}
} 30 |
31 |
32 | ); 33 | }; 34 | 35 | DateTimePicker.propTypes = { 36 | name: React.PropTypes.string.isRequired, 37 | label: React.PropTypes.string.isRequired, 38 | onChange: React.PropTypes.func.isRequired, 39 | value: React.PropTypes.string, 40 | error: React.PropTypes.string 41 | }; 42 | 43 | export default DateTimePicker; -------------------------------------------------------------------------------- /client/src/components/common/DisplayRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DisplayRow = ({label, value}) => { 4 | 5 | return ( 6 |
7 | 8 | 9 |
{value}
10 |
11 | ); 12 | }; 13 | 14 | DisplayRow.propTypes = { 15 | label: React.PropTypes.string.isRequired, 16 | value: React.PropTypes.string 17 | }; 18 | 19 | export default DisplayRow; -------------------------------------------------------------------------------- /client/src/components/common/NumbertInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NumberInput = ({name, label, onChange, value, error}) => { 4 | let wrapperClass = 'form-group'; 5 | if (error && error.length > 0) { 6 | wrapperClass += ' has-error'; 7 | } 8 | 9 | return ( 10 |
11 | 12 |
13 | 20 | {error &&
{error}
} 21 |
22 |
23 | ); 24 | }; 25 | 26 | NumberInput.propTypes = { 27 | name: React.PropTypes.string.isRequired, 28 | label: React.PropTypes.string.isRequired, 29 | onChange: React.PropTypes.func.isRequired, 30 | value: React.PropTypes.string, 31 | error: React.PropTypes.string 32 | }; 33 | 34 | export default NumberInput; -------------------------------------------------------------------------------- /client/src/components/common/SelectInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | 4 | const SelectInput = ({name, label, onChange, defaultOption, value, error, options}) => { 5 | return ( 6 |
7 | 8 |
9 | 19 | {error &&
{error}
} 20 |
21 |
22 | ); 23 | }; 24 | 25 | SelectInput.propTypes = { 26 | name: React.PropTypes.string.isRequired, 27 | label: React.PropTypes.string.isRequired, 28 | onChange: React.PropTypes.func.isRequired, 29 | defaultOption: React.PropTypes.string, 30 | value: React.PropTypes.string, 31 | error: React.PropTypes.string, 32 | options: React.PropTypes.arrayOf(React.PropTypes.object) 33 | }; 34 | 35 | export default SelectInput; 36 | -------------------------------------------------------------------------------- /client/src/components/common/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TextInput = ({name, label, onChange, placeholder, value, error}) => { 4 | let wrapperClass = 'form-group'; 5 | if (error && error.length > 0) { 6 | wrapperClass += ' has-error'; 7 | } 8 | 9 | return ( 10 |
11 | 12 |
13 | 20 | {error &&
{error}
} 21 |
22 |
23 | ); 24 | }; 25 | 26 | TextInput.propTypes = { 27 | name: React.PropTypes.string.isRequired, 28 | label: React.PropTypes.string.isRequired, 29 | onChange: React.PropTypes.func.isRequired, 30 | placeholder: React.PropTypes.string, 31 | value: React.PropTypes.string, 32 | error: React.PropTypes.string 33 | }; 34 | 35 | export default TextInput; -------------------------------------------------------------------------------- /client/src/components/courses/CourseDelete.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import {Modal, Button} from 'react-bootstrap'; 5 | import helper from '../../helpers/uiHelper'; 6 | import _ from 'lodash'; 7 | import * as courseActions from '../../actions/courseActions'; 8 | 9 | class CourseDelete extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | course: _.assign({}, props.course), 15 | visible: props.visible, 16 | close: props.close 17 | }; 18 | 19 | this.deleteCourse = this.deleteCourse.bind(this); 20 | } 21 | 22 | deleteCourse(event) { 23 | event.preventDefault(); 24 | 25 | this.props.actions.deleteCourse(this.props.course.id) 26 | .then(() => { 27 | this.props.close(); 28 | 29 | helper.showMessage('Course deleted'); 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 | Confirmation 39 | 40 | 41 |

Are you sure you want to delete this?

42 |
43 | 44 | 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | CourseDelete.propTypes = { 54 | course: React.PropTypes.object.isRequired, 55 | actions: React.PropTypes.object.isRequired, 56 | visible: React.PropTypes.bool.isRequired, 57 | close: React.PropTypes.func.isRequired 58 | }; 59 | 60 | function mapStateToProps(state) { 61 | return { 62 | course: _.cloneDeep(state.course.current) 63 | }; 64 | } 65 | 66 | function mapDispatchToProps(dispatch) { 67 | return { 68 | actions: bindActionCreators(courseActions, dispatch) 69 | }; 70 | } 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(CourseDelete); -------------------------------------------------------------------------------- /client/src/components/courses/CourseDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {Modal, Button} from 'react-bootstrap'; 4 | import _ from 'lodash'; 5 | import DisplayRow from '../common/DisplayRow'; 6 | 7 | class CourseDetails extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | course: _.assign({}, props.course), 13 | visible: props.visible, 14 | close: props.close 15 | }; 16 | } 17 | 18 | render() { 19 | let course = this.props.course; 20 | let department = course.department; 21 | let departmentName = department ? department.name : ''; 22 | let courseNumber = course.number ? course.number.toString() : ''; 23 | let courseCredits = course.credits ? course.credits.toString() : ''; 24 | 25 | return ( 26 |
27 | 28 | 29 | Course Details 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | CourseDetails.propTypes = { 52 | course: React.PropTypes.object.isRequired, 53 | visible: React.PropTypes.bool.isRequired, 54 | close: React.PropTypes.func.isRequired 55 | }; 56 | 57 | function mapStateToProps(state) { 58 | return { 59 | course: _.cloneDeep(state.course.current) 60 | }; 61 | } 62 | 63 | export default connect(mapStateToProps)(CourseDetails); -------------------------------------------------------------------------------- /client/src/components/courses/CourseForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | import NumberInput from '../common/NumbertInput'; 4 | import SelectInput from '../common/SelectInput'; 5 | 6 | const CourseForm = ({course, allDepartments, onChange, errors}) => { 7 | 8 | return ( 9 |
10 | 17 | 18 | 26 | 27 | 34 | 35 | 44 | 45 | ); 46 | }; 47 | 48 | CourseForm.propTypes = { 49 | course: React.PropTypes.object.isRequired, 50 | allDepartments: React.PropTypes.array, 51 | onChange: React.PropTypes.func.isRequired, 52 | errors: React.PropTypes.object 53 | }; 54 | 55 | export default CourseForm; -------------------------------------------------------------------------------- /client/src/components/courses/CoursesFilter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CoursesFilter = ({departmentId, departments, onChange, onClick}) => { 4 | let filterStyle = {marginTop: 10 + 'px'}; 5 | let selectStyle = {marginRight: 10 + 'px', marginLeft: 10 + 'px'}; 6 | 7 | return ( 8 |
9 | Select Department: 10 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | CoursesFilter.propTypes = { 28 | departmentId: React.PropTypes.string.isRequired, 29 | departments: React.PropTypes.array.isRequired, 30 | onChange: React.PropTypes.func.isRequired, 31 | onClick: React.PropTypes.func.isRequired 32 | }; 33 | 34 | export default CoursesFilter; 35 | -------------------------------------------------------------------------------- /client/src/components/courses/CoursesList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import CoursesRow from './CoursesRow'; 4 | 5 | const CoursesList = ({courses, onSaveClick, onDetailsClick, onDeleteClick}) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {courses.map(course => 19 | onSaveClick(course.id)} 21 | onDetailsClick={() => onDetailsClick(course.id)} 22 | onDeleteClick={() => onDeleteClick(course.id)} 23 | /> 24 | )} 25 | 26 |
NumberTitleCreditsDepartment
27 | ); 28 | }; 29 | 30 | CoursesList.propTypes = { 31 | courses: React.PropTypes.array.isRequired, 32 | onSaveClick: React.PropTypes.func.isRequired, 33 | onDetailsClick: React.PropTypes.func.isRequired, 34 | onDeleteClick: React.PropTypes.func.isRequired 35 | }; 36 | 37 | export default CoursesList; 38 | -------------------------------------------------------------------------------- /client/src/components/courses/CoursesRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CoursesRow = ({course, onSaveClick, onDetailsClick, onDeleteClick}) => { 4 | return ( 5 | 6 | {course.number} 7 | {course.title} 8 | {course.credits} 9 | {course.department.name} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | CoursesRow.propTypes = { 22 | course: React.PropTypes.object.isRequired, 23 | onSaveClick: React.PropTypes.func.isRequired, 24 | onDetailsClick: React.PropTypes.func.isRequired, 25 | onDeleteClick: React.PropTypes.func.isRequired 26 | }; 27 | 28 | export default CoursesRow; 29 | -------------------------------------------------------------------------------- /client/src/components/department/DepartmentDelete.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import {Modal, Button} from 'react-bootstrap'; 5 | import helper from '../../helpers/uiHelper'; 6 | import _ from 'lodash'; 7 | import * as departmentActions from '../../actions/departmentActions'; 8 | 9 | class DepartmentDelete extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | department: _.assign({}, props.department), 15 | visible: props.visible, 16 | close: props.close 17 | }; 18 | 19 | this.deleteDepartment = this.deleteDepartment.bind(this); 20 | } 21 | 22 | deleteDepartment(event) { 23 | event.preventDefault(); 24 | 25 | this.props.actions.deleteDepartment(this.props.department.id) 26 | .then(() => { 27 | this.props.close(); 28 | 29 | helper.showMessage('Department deleted'); 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 | Confirmation 39 | 40 | 41 |

Are you sure you want to delete this?

42 |
43 | 44 | 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | DepartmentDelete.propTypes = { 54 | department: React.PropTypes.object.isRequired, 55 | actions: React.PropTypes.object.isRequired, 56 | visible: React.PropTypes.bool.isRequired, 57 | close: React.PropTypes.func.isRequired 58 | }; 59 | 60 | function mapStateToProps(state) { 61 | return { 62 | department: _.cloneDeep(state.department.current) 63 | }; 64 | } 65 | 66 | function mapDispatchToProps(dispatch) { 67 | return { 68 | actions: bindActionCreators(departmentActions, dispatch) 69 | }; 70 | } 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(DepartmentDelete); -------------------------------------------------------------------------------- /client/src/components/department/DepartmentDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {Modal, Button} from 'react-bootstrap'; 4 | import _ from 'lodash'; 5 | import DisplayRow from '../common/DisplayRow'; 6 | import dateFormatter from '../../formatters/dateFormatter'; 7 | import currencyFormatter from '../../formatters/currencyFormatter'; 8 | 9 | class DepartmentDetails extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | department: _.assign({}, props.department), 15 | visible: props.visible, 16 | close: props.close 17 | }; 18 | } 19 | 20 | render() { 21 | let department = this.props.department; 22 | let instructor = department.instructor; 23 | let instructorName = instructor ? `${instructor.lastName}, ${instructor.firstName}` : ''; 24 | let startDate = dateFormatter.date(department.startDate); 25 | let budgetDisplay = currencyFormatter.money(department.budget); 26 | 27 | return ( 28 |
29 | 30 | 31 | Department Details 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | DepartmentDetails.propTypes = { 54 | department: React.PropTypes.object.isRequired, 55 | visible: React.PropTypes.bool.isRequired, 56 | close: React.PropTypes.func.isRequired 57 | }; 58 | 59 | function mapStateToProps(state) { 60 | return { 61 | department: _.cloneDeep(state.department.current) 62 | }; 63 | } 64 | 65 | export default connect(mapStateToProps)(DepartmentDetails); -------------------------------------------------------------------------------- /client/src/components/department/DepartmentForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | import NumberInput from '../common/NumbertInput'; 4 | import SelectInput from '../common/SelectInput'; 5 | import DateTimePicker from '../common/DateTimePicker'; 6 | import _ from 'lodash'; 7 | 8 | const DepartmentForm = ({department, allInstructors, onChange, errors}) => { 9 | 10 | return ( 11 |
12 | 20 | 21 | 28 | 29 | 36 | 37 | 46 | 47 | ); 48 | }; 49 | 50 | DepartmentForm.propTypes = { 51 | department: React.PropTypes.object.isRequired, 52 | allInstructors: React.PropTypes.array, 53 | onChange: React.PropTypes.func.isRequired, 54 | errors: React.PropTypes.object 55 | }; 56 | 57 | export default DepartmentForm; -------------------------------------------------------------------------------- /client/src/components/department/DepartmentRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dateFormatter from '../../formatters/dateFormatter'; 3 | import personFormat from '../../formatters/personFormatter'; 4 | import currencyFormatter from '../../formatters/currencyFormatter'; 5 | 6 | const DepartmentRow = ({department, onSaveClick, onDetailsClick, onDeleteClick}) => { 7 | let instructor = department.instructor; 8 | 9 | let fullName = instructor ? 10 | personFormat.fullName(instructor.firstName, instructor.lastName) : ''; 11 | 12 | let startDateDisplay = dateFormatter.date(department.startDate); 13 | let budgetDisplay = currencyFormatter.money(department.budget); 14 | 15 | return ( 16 | 17 | {department.name} 18 | {budgetDisplay} 19 | {startDateDisplay} 20 | {fullName} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | DepartmentRow.propTypes = { 33 | department: React.PropTypes.object.isRequired, 34 | onSaveClick: React.PropTypes.func.isRequired, 35 | onDetailsClick: React.PropTypes.func.isRequired, 36 | onDeleteClick: React.PropTypes.func.isRequired, 37 | }; 38 | 39 | export default DepartmentRow; 40 | -------------------------------------------------------------------------------- /client/src/components/department/DepartmentsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import DepartmentRow from './DepartmentRow'; 4 | 5 | const DepartmentsList = ({departments, onSaveClick, onDetailsClick, onDeleteClick}) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {departments.map(department => 19 | onSaveClick(department.id)} 21 | onDetailsClick={() => onDetailsClick(department.id)} 22 | onDeleteClick={() => onDeleteClick(department.id)} 23 | /> 24 | )} 25 | 26 |
NameBudgetStart DateAdministrator
27 | ); 28 | }; 29 | 30 | DepartmentsList.propTypes = { 31 | departments: React.PropTypes.array.isRequired, 32 | onSaveClick: React.PropTypes.func.isRequired, 33 | onDetailsClick: React.PropTypes.func.isRequired, 34 | onDeleteClick: React.PropTypes.func.isRequired 35 | }; 36 | 37 | export default DepartmentsList; 38 | -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorCourseRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const InstructorCourseRow = (props) => { 4 | let course = props.course; 5 | 6 | let activeClass = props.selectedCourseId === course.id ? 'success' : ''; 7 | 8 | return ( 9 | 10 | 11 | 12 | 13 | {course.number} 14 | {course.title} 15 | {course.department.name} 16 | 17 | ); 18 | }; 19 | 20 | InstructorCourseRow.propTypes = { 21 | course: React.PropTypes.object.isRequired, 22 | selectedCourseId: React.PropTypes.number.isRequired, 23 | onSelectClick: React.PropTypes.func.isRequired 24 | }; 25 | 26 | export default InstructorCourseRow; 27 | -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorCoursesList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import _ from 'lodash'; 5 | import * as instructorActions from '../../actions/instructorActions'; 6 | import InstructorCourseRow from './InstructorCourseRow'; 7 | 8 | class InstructorCoursesList extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | instructor: _.assign({}, props.instructor), 14 | visible: props.visible 15 | }; 16 | } 17 | 18 | render() { 19 | let instructor = this.props.instructor; 20 | let courses = (instructor && instructor.courses) ? instructor.courses : []; 21 | 22 | let style = this.props.visible ? {display: 'block'} : {display: 'none'}; 23 | 24 | return ( 25 |
26 |

Courses Taught by Selected Instructor

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {courses.map(course => 39 | this.props.onSelectClick(course.id)} 42 | /> 43 | )} 44 | 45 |
NumberTitleDepartment
46 |
47 | ); 48 | } 49 | } 50 | 51 | InstructorCoursesList.propTypes = { 52 | instructor: React.PropTypes.object.isRequired, 53 | actions: React.PropTypes.object.isRequired, 54 | visible: React.PropTypes.bool.isRequired, 55 | selectedCourseId: React.PropTypes.number.isRequired, 56 | onSelectClick: React.PropTypes.func.isRequired 57 | }; 58 | 59 | function mapStateToProps(state) { 60 | return { 61 | instructor: _.cloneDeep(state.instructor.current) 62 | }; 63 | } 64 | 65 | function mapDispatchToProps(dispatch) { 66 | return { 67 | actions: bindActionCreators(instructorActions, dispatch) 68 | }; 69 | } 70 | 71 | export default connect(mapStateToProps, mapDispatchToProps)(InstructorCoursesList); -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorDelete.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import {Modal, Button} from 'react-bootstrap'; 5 | import helper from '../../helpers/uiHelper'; 6 | import _ from 'lodash'; 7 | import * as instructorActions from '../../actions/instructorActions'; 8 | 9 | class InstructorDelete extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | instructor: _.assign({}, props.instructor), 15 | visible: props.visible, 16 | close: props.close 17 | }; 18 | 19 | this.deleteInstructor = this.deleteInstructor.bind(this); 20 | } 21 | 22 | deleteInstructor(event) { 23 | event.preventDefault(); 24 | 25 | this.props.actions.deleteInstructor(this.props.instructor.id) 26 | .then(() => { 27 | this.props.close(true); 28 | 29 | helper.showMessage('Instructor deleted'); 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 | Confirmation 39 | 40 | 41 |

Are you sure you want to delete this?

42 |
43 | 44 | 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | InstructorDelete.propTypes = { 54 | instructor: React.PropTypes.object.isRequired, 55 | actions: React.PropTypes.object.isRequired, 56 | visible: React.PropTypes.bool.isRequired, 57 | close: React.PropTypes.func.isRequired 58 | }; 59 | 60 | function mapStateToProps(state) { 61 | return { 62 | instructor: _.cloneDeep(state.instructor.current) 63 | }; 64 | } 65 | 66 | function mapDispatchToProps(dispatch) { 67 | return { 68 | actions: bindActionCreators(instructorActions, dispatch) 69 | }; 70 | } 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(InstructorDelete); -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {Modal, Button} from 'react-bootstrap'; 4 | import _ from 'lodash'; 5 | import DisplayRow from '../common/DisplayRow'; 6 | import dateFormatter from '../../formatters/dateFormatter'; 7 | 8 | class InstructorDetails extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | instructor: _.assign({}, props.instructor), 14 | visible: props.visible, 15 | close: props.close 16 | }; 17 | } 18 | 19 | render() { 20 | let instructor = this.props.instructor; 21 | 22 | let office = instructor.officeAssignment; 23 | let location = office ? office.location : 'No office'; 24 | let hireDate = dateFormatter.date(instructor.hireDate); 25 | 26 | return ( 27 |
28 | 29 | 30 | Instructor Details 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 |
43 | 44 | 45 | 46 |
47 |
48 | ); 49 | } 50 | } 51 | 52 | InstructorDetails.propTypes = { 53 | instructor: React.PropTypes.object.isRequired, 54 | visible: React.PropTypes.bool.isRequired, 55 | close: React.PropTypes.func.isRequired 56 | }; 57 | 58 | function mapStateToProps(state) { 59 | return { 60 | instructor: _.cloneDeep(state.instructor.current) 61 | }; 62 | } 63 | 64 | export default connect(mapStateToProps)(InstructorDetails); -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | import DateTimePicker from '../common/DateTimePicker'; 4 | import CheckBox from '../common/CheckBox'; 5 | import _ from 'lodash'; 6 | 7 | const InstructorForm = ({instructor, allCourses, onChange, errors}) => { 8 | let office = instructor.officeAssignment ? instructor.officeAssignment.location : ''; 9 | 10 | function isCheckedCourse(courseId) { 11 | let exist = _.find(instructor.courses, (course) => { 12 | return course.id === courseId; 13 | }); 14 | 15 | return exist ? true : false; 16 | } 17 | 18 | return ( 19 |
20 | 28 | 29 | 37 | 38 | 45 | 46 | 54 | 55 | {allCourses.map(course => 56 | 63 | )} 64 | 65 | ); 66 | }; 67 | 68 | InstructorForm.propTypes = { 69 | instructor: React.PropTypes.object.isRequired, 70 | allCourses: React.PropTypes.array, 71 | onChange: React.PropTypes.func.isRequired, 72 | errors: React.PropTypes.object 73 | }; 74 | 75 | export default InstructorForm; -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InstructorRow from './InstructorRow'; 3 | 4 | const InstructorList = ({instructors, selectedInstructorId, onSelectClick, onSaveClick, onDetailsClick, onDeleteClick}) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {instructors.map(instructor => 19 | onSelectClick(instructor.id)} 22 | onSaveClick={() => onSaveClick(instructor.id)} 23 | onDetailsClick={() => onDetailsClick(instructor.id)} 24 | onDeleteClick={() => onDeleteClick(instructor.id)} 25 | /> 26 | )} 27 | 28 |
Last NameFirst NameHire DateOfficeCourses
29 | ); 30 | }; 31 | 32 | InstructorList.propTypes = { 33 | instructors: React.PropTypes.array.isRequired, 34 | selectedInstructorId: React.PropTypes.number.isRequired, 35 | onSaveClick: React.PropTypes.func.isRequired, 36 | onSelectClick: React.PropTypes.func.isRequired, 37 | onDetailsClick: React.PropTypes.func.isRequired, 38 | onDeleteClick: React.PropTypes.func.isRequired 39 | }; 40 | 41 | export default InstructorList; 42 | -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dateFormatter from '../../formatters/dateFormatter'; 3 | 4 | const InstructorRow = (props) => { 5 | let instructor = props.instructor; 6 | 7 | let office = instructor.officeAssignment ? instructor.officeAssignment.location : ''; 8 | let hireDateDisplay = dateFormatter.date(instructor.hireDate); 9 | 10 | let activeClass = props.selectedInstructorId === instructor.id ? 'success' : ''; 11 | 12 | return ( 13 | 14 | {instructor.lastName} 15 | {instructor.firstName} 16 | {hireDateDisplay} 17 | {office} 18 | {instructor.courses.map(course => 19 |
{course.number} {course.title}
20 | )} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | InstructorRow.propTypes = { 36 | instructor: React.PropTypes.object.isRequired, 37 | selectedInstructorId: React.PropTypes.number.isRequired, 38 | onSelectClick: React.PropTypes.func.isRequired, 39 | onSaveClick: React.PropTypes.func.isRequired, 40 | onDetailsClick: React.PropTypes.func.isRequired, 41 | onDeleteClick: React.PropTypes.func.isRequired 42 | }; 43 | 44 | export default InstructorRow; 45 | -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorStudentRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import personFormat from '../../formatters/personFormatter'; 3 | 4 | const InstructorStudentRow = (props) => { 5 | let enrollment = props.enrollment; 6 | 7 | let fullName = enrollment ? personFormat.fullName(enrollment.student.firstName, enrollment.student.lastName) : ''; 8 | let grade = enrollment.grade ? enrollment.grade : 'No grade'; 9 | 10 | return ( 11 | 12 | {fullName} 13 | {grade} 14 | 15 | ); 16 | }; 17 | 18 | InstructorStudentRow.propTypes = { 19 | enrollment: React.PropTypes.object.isRequired 20 | }; 21 | 22 | export default InstructorStudentRow; 23 | -------------------------------------------------------------------------------- /client/src/components/instructor/InstructorStudentsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import InstructorStudentRow from './InstructorStudentRow'; 3 | 4 | const InstructorStudentsList = ({enrollments, visible}) => { 5 | let style = visible ? {display: 'block'} : {display: 'none'}; 6 | 7 | return ( 8 |
9 |

Students Enrolled in Selected Course

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {enrollments.map(enrollment => 20 | 21 | )} 22 | 23 |
NameGrade
24 |
25 | ); 26 | }; 27 | 28 | InstructorStudentsList.propTypes = { 29 | visible: React.PropTypes.bool.isRequired, 30 | enrollments: React.PropTypes.array.isRequired 31 | }; 32 | 33 | export default InstructorStudentsList; 34 | -------------------------------------------------------------------------------- /client/src/components/student/StudentDelete.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {bindActionCreators} from 'redux'; 4 | import {Modal, Button} from 'react-bootstrap'; 5 | import helper from '../../helpers/uiHelper'; 6 | import _ from 'lodash'; 7 | import * as studentActions from '../../actions/studentActions'; 8 | 9 | class StudentDelete extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | student: _.assign({}, props.student), 15 | visible: props.visible, 16 | close: props.close 17 | }; 18 | 19 | this.deleteStudent = this.deleteStudent.bind(this); 20 | } 21 | 22 | deleteStudent(event) { 23 | event.preventDefault(); 24 | 25 | this.props.actions.deleteStudent(this.props.student.id) 26 | .then(() => { 27 | this.props.close(); 28 | 29 | helper.showMessage('Student deleted'); 30 | }); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | 37 | 38 | Confirmation 39 | 40 | 41 |

Are you sure you want to delete this?

42 |
43 | 44 | 45 | 46 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | StudentDelete.propTypes = { 54 | student: React.PropTypes.object.isRequired, 55 | actions: React.PropTypes.object.isRequired, 56 | visible: React.PropTypes.bool.isRequired, 57 | close: React.PropTypes.func.isRequired 58 | }; 59 | 60 | function mapStateToProps(state) { 61 | return { 62 | student: _.cloneDeep(state.student.current) 63 | }; 64 | } 65 | 66 | function mapDispatchToProps(dispatch) { 67 | return { 68 | actions: bindActionCreators(studentActions, dispatch) 69 | }; 70 | } 71 | 72 | export default connect(mapStateToProps, mapDispatchToProps)(StudentDelete); -------------------------------------------------------------------------------- /client/src/components/student/StudentDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {Modal, Button} from 'react-bootstrap'; 4 | import _ from 'lodash'; 5 | import dateFormatter from '../../formatters/dateFormatter'; 6 | import StudentEnrollmentsList from './StudentEnrollmentsList'; 7 | import DisplayRow from '../common/DisplayRow'; 8 | 9 | class StudentDetails extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | student: _.assign({}, props.student), 15 | visible: props.visible, 16 | close: props.close 17 | }; 18 | } 19 | 20 | render() { 21 | let student = this.props.student; 22 | 23 | let enrollmentDate = dateFormatter.date(student.enrollmentDate); 24 | let enrollments = (student && student.enrollments) ? student.enrollments : []; 25 | let enrollmentsStyle = _.isEmpty(enrollments) ? {display: 'none'} : {}; 26 | 27 | return ( 28 |
29 | 30 | 31 | Student Details 32 | 33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 |
53 |
54 | ); 55 | } 56 | } 57 | 58 | StudentDetails.propTypes = { 59 | student: React.PropTypes.object.isRequired, 60 | visible: React.PropTypes.bool.isRequired, 61 | close: React.PropTypes.func.isRequired 62 | }; 63 | 64 | function mapStateToProps(state) { 65 | return { 66 | student: _.cloneDeep(state.student.current) 67 | }; 68 | } 69 | 70 | export default connect(mapStateToProps)(StudentDetails); -------------------------------------------------------------------------------- /client/src/components/student/StudentEnrollmentsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import StudentEnrollmentsRow from './StudentEnrollmentsRow'; 3 | 4 | const StudentEnrollmentsList = ({enrollments}) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {enrollments.map(enrollment => 15 | 16 | )} 17 | 18 |
Course TitleGrade
19 | ); 20 | }; 21 | 22 | StudentEnrollmentsList.propTypes = { 23 | enrollments: React.PropTypes.array.isRequired 24 | }; 25 | 26 | export default StudentEnrollmentsList; 27 | -------------------------------------------------------------------------------- /client/src/components/student/StudentEnrollmentsRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const StudentEnrollmentsRow = ({enrollment}) => { 4 | let grade = enrollment.grade ? enrollment.grade : 'No grade'; 5 | 6 | return ( 7 | 8 | {enrollment.course.title} 9 | {grade} 10 | 11 | ); 12 | }; 13 | 14 | StudentEnrollmentsRow.propTypes = { 15 | enrollment: React.PropTypes.object.isRequired 16 | }; 17 | 18 | export default StudentEnrollmentsRow; 19 | -------------------------------------------------------------------------------- /client/src/components/student/StudentForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextInput from '../common/TextInput'; 3 | import DateTimePicker from '../common/DateTimePicker'; 4 | 5 | const StudentForm = ({student, onChange, errors}) => { 6 | 7 | return ( 8 |
9 | 17 | 18 | 26 | 27 | 34 | 35 | ); 36 | }; 37 | 38 | StudentForm.propTypes = { 39 | student: React.PropTypes.object.isRequired, 40 | onChange: React.PropTypes.func.isRequired, 41 | errors: React.PropTypes.object 42 | }; 43 | 44 | export default StudentForm; -------------------------------------------------------------------------------- /client/src/components/student/StudentRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dateFormatter from '../../formatters/dateFormatter'; 3 | 4 | const StudentRow = ({student, onSaveClick, onDetailsClick, onDeleteClick}) => { 5 | let enrollmentDateDisplay = dateFormatter.date(student.enrollmentDate); 6 | 7 | return ( 8 | 9 | {student.lastName} 10 | {student.firstName} 11 | {enrollmentDateDisplay} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | StudentRow.propTypes = { 24 | student: React.PropTypes.object.isRequired, 25 | onSaveClick: React.PropTypes.func.isRequired, 26 | onDetailsClick: React.PropTypes.func.isRequired, 27 | onDeleteClick: React.PropTypes.func.isRequired 28 | }; 29 | 30 | export default StudentRow; 31 | -------------------------------------------------------------------------------- /client/src/components/student/StudentSearch.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const StudentSearch = ({search, onChange, onKeyPress, onClick}) => { 4 | let searchStyle = {marginTop: 10 + 'px'}; 5 | let inputStyle = {marginRight: 10 + 'px', marginLeft: 10 + 'px'}; 6 | 7 | return ( 8 |
9 | Find by name: 10 | 15 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | StudentSearch.propTypes = { 23 | search: React.PropTypes.string.isRequired, 24 | onChange: React.PropTypes.func.isRequired, 25 | onKeyPress: React.PropTypes.func.isRequired, 26 | onClick: React.PropTypes.func.isRequired 27 | }; 28 | 29 | export default StudentSearch; 30 | -------------------------------------------------------------------------------- /client/src/components/student/StudentStatisticsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import StudentStatisticsRow from './StudentStatisticsRow'; 4 | 5 | const StudentStatisticsList = ({statistics}) => { 6 | let id = 1; //for key 7 | 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {statistics.map(statistic => 18 | 19 | )} 20 | 21 |
Enrollment DateStudents
22 | ); 23 | }; 24 | 25 | StudentStatisticsList.propTypes = { 26 | statistics: React.PropTypes.array.isRequired 27 | }; 28 | 29 | export default StudentStatisticsList; 30 | -------------------------------------------------------------------------------- /client/src/components/student/StudentStatisticsRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dateFormatter from '../../formatters/dateFormatter'; 3 | 4 | const StudentStatisticsRow = ({statistic}) => { 5 | let enrollmentDateDisplay = dateFormatter.date(statistic.enrollmentDate); 6 | 7 | return ( 8 | 9 | {enrollmentDateDisplay} 10 | {statistic.studentCount} 11 | 12 | ); 13 | }; 14 | 15 | StudentStatisticsRow.propTypes = { 16 | statistic: React.PropTypes.object.isRequired 17 | }; 18 | 19 | export default StudentStatisticsRow; 20 | -------------------------------------------------------------------------------- /client/src/components/student/StudentsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import StudentRow from './StudentRow'; 4 | 5 | const StudentsList = ({students, onSortClick, onSaveClick, onDetailsClick, onDeleteClick}) => { 6 | let showTable = _.isEmpty(students) ? {display: 'none'} : {}; 7 | 8 | return ( 9 | 10 | 11 | 12 | 15 | 16 | 19 | 20 | 21 | 22 | 23 | {students.map(student => 24 | onSaveClick(student.id)} 26 | onDetailsClick={() => onDetailsClick(student.id)} 27 | onDeleteClick={() => onDeleteClick(student.id)} 28 | /> 29 | )} 30 | 31 |
13 | Last Name 14 | First Name 17 | Enrollment Date 18 |
32 | ); 33 | }; 34 | 35 | StudentsList.propTypes = { 36 | students: React.PropTypes.array.isRequired, 37 | onSortClick: React.PropTypes.func.isRequired, 38 | onSaveClick: React.PropTypes.func.isRequired, 39 | onDetailsClick: React.PropTypes.func.isRequired, 40 | onDeleteClick: React.PropTypes.func.isRequired 41 | }; 42 | 43 | export default StudentsList; 44 | -------------------------------------------------------------------------------- /client/src/formatters/currencyFormatter.js: -------------------------------------------------------------------------------- 1 | import config from '../helpers/clientConfig'; 2 | import _ from 'lodash'; 3 | 4 | export default { 5 | money: moneyFormat 6 | }; 7 | 8 | function moneyFormat(amount) { 9 | let amountNum = parseFloat(amount); 10 | 11 | if (!_.isNumber(amountNum)) return 'invalid value'; 12 | 13 | let amountFormatted = amountNum.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,'); 14 | 15 | return `${config.format.currencySymbol} ${amountFormatted}`; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/formatters/dateFormatter.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import config from '../helpers/clientConfig'; 3 | 4 | export default { 5 | currentDate: getCurrentDate, 6 | date: formatDate, 7 | parse: parseDate, 8 | isValid: isValidDate, 9 | currentYear: getCurrentYear 10 | }; 11 | 12 | function getCurrentDate() { 13 | var now = new Date(); 14 | return now.toISOString(); 15 | } 16 | 17 | function formatDate(dateStr) { 18 | if (!dateStr) dateStr = getCurrentDate(); 19 | 20 | return moment(dateStr).format(config.format.date); 21 | } 22 | 23 | function parseDate(dateStr) { 24 | return moment(dateStr).toDate(); 25 | } 26 | 27 | function isValidDate(dateStr) { 28 | return moment(dateStr, moment.ISO_8601, true).isValid(); 29 | } 30 | 31 | function getCurrentYear() { 32 | return moment().format(config.format.year); 33 | } -------------------------------------------------------------------------------- /client/src/formatters/entityFromatter.js: -------------------------------------------------------------------------------- 1 | export function instructorSelectListItem(instructors) { 2 | return instructors.map(instructor => { 3 | return { 4 | value: instructor.id, 5 | text: instructor.lastName + ', ' + instructor.firstName 6 | }; 7 | }); 8 | } 9 | 10 | export function courseSelectListItem(courses) { 11 | return courses.map(course => { 12 | return { 13 | value: course.id, 14 | text: course.number + ' ' + course.title 15 | }; 16 | }); 17 | } 18 | 19 | export function departmentSelectListItem(departments) { 20 | return departments.map(department => { 21 | return { 22 | value: department.id, 23 | text: department.name 24 | }; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /client/src/formatters/personFormatter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | fullName: getFullName 3 | }; 4 | 5 | function getFullName(firstName, lastName) { 6 | return `${lastName}, ${firstName}`; 7 | } -------------------------------------------------------------------------------- /client/src/helpers/clientConfig.js: -------------------------------------------------------------------------------- 1 | function loadConfig() { 2 | let configData = require('../../config/config.json'); 3 | 4 | configData.isDevLocal = process.env.NODE_ENV === 'development'; 5 | 6 | return configData; 7 | } 8 | 9 | let configValues = loadConfig(); 10 | 11 | export default configValues; 12 | 13 | -------------------------------------------------------------------------------- /client/src/helpers/httpHelper.js: -------------------------------------------------------------------------------- 1 | import uiHelper from './uiHelper'; 2 | import Promise from 'bluebird'; 3 | import config from './clientConfig'; 4 | import $ from 'jquery'; 5 | 6 | export default { 7 | get: httpGet, 8 | post: httpPost, 9 | put: httpPut, 10 | delete: httpDelete 11 | }; 12 | 13 | function ajaxRequest(httpVerb, url, data) { 14 | let fullUrl = config.isDevLocal ? config.proxy + url : url; 15 | 16 | return new Promise((resolve, reject) => { 17 | $.ajax({ 18 | type: httpVerb, 19 | url: fullUrl, 20 | dataType: 'json', 21 | cache: false, 22 | data: data, 23 | timeout: 0, 24 | success: function (response) { 25 | if (response.status === 'failure') { 26 | let message = response.message; 27 | if (!message) message = 'Server Error'; 28 | 29 | uiHelper.showError(message); 30 | 31 | reject(new Error(message)); 32 | } else { 33 | resolve(response.data); 34 | } 35 | }, 36 | error: function (err) { 37 | let errMessage = getErrorMessage(err); 38 | uiHelper.showError(errMessage); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | function getErrorMessage(err) { 45 | if (err.status === 404 || err.data.error) return err.data.error; 46 | return 'API error'; 47 | } 48 | 49 | function httpGet(url, data) { 50 | return ajaxRequest('GET', url, data); 51 | } 52 | 53 | function httpPost(url, data) { 54 | return ajaxRequest('POST', url, data); 55 | } 56 | 57 | function httpPut(url, data) { 58 | return ajaxRequest('PUT', url, data); 59 | } 60 | 61 | function httpDelete(url, data) { 62 | return ajaxRequest('DELETE', url, data); 63 | } -------------------------------------------------------------------------------- /client/src/helpers/uiHelper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import toastr from 'toastr'; 3 | 4 | export default { 5 | showMessage, 6 | showWarning, 7 | showError 8 | }; 9 | 10 | function setToasterOptions() { 11 | toastr.options.positionClass = 'toast-bottom-right'; 12 | } 13 | 14 | setToasterOptions(); 15 | 16 | function showError(err) { 17 | let errorMessage = err; 18 | 19 | if (_.isError(err)) { 20 | errorMessage = err.message; 21 | } 22 | 23 | toastr.error(errorMessage); 24 | } 25 | 26 | function showWarning(message) { 27 | toastr.warning(message); 28 | } 29 | 30 | function showMessage(message) { 31 | toastr.success(message); 32 | } -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import '../styles/app.css'; 2 | 3 | import React from 'react'; 4 | import {render} from 'react-dom'; 5 | import configureStore from './store/configureStore'; 6 | import {Provider} from 'react-redux'; 7 | import {Router} from 'react-router'; 8 | import {browserHistory} from 'react-router'; 9 | import routes from './routes'; 10 | import {loadDepartments} from './actions/departmentActions'; 11 | import {loadInstructors} from './actions/instructorActions'; 12 | 13 | const store = configureStore(); 14 | 15 | //TODO move into componentWillMount (as for about page) 16 | store.dispatch(loadDepartments()); 17 | store.dispatch(loadInstructors()); 18 | 19 | render( 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ); 25 | -------------------------------------------------------------------------------- /client/src/reducers/ajaxStatusReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | 4 | function actionTypeEndsInSuccess(type) { 5 | return type.substring(type.length - 8) === '_SUCCESS'; 6 | } 7 | 8 | export default function ajaxStatusReducer(state = initialState.ajaxCallsInProgress, action) { 9 | if (action.type === types.BEGIN_AJAX_CALL) { 10 | return state + 1; 11 | } else if (action.type === types.AJAX_CALL_ERROR || 12 | actionTypeEndsInSuccess(action.type)) { 13 | return state - 1; 14 | } 15 | 16 | 17 | return state; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/reducers/courseReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | import _ from 'lodash'; 4 | 5 | export default function courseReducer(state = initialState.course, action) { 6 | switch (action.type) { 7 | case types.LOAD_COURSES_SUCCESS: 8 | return _.assign({}, state, {list: action.courses}); 9 | 10 | case types.LOAD_COURSE_SUCCESS: 11 | return _.assign({}, state, {current: action.course}); 12 | 13 | case types.CREATE_COURSE_SUCCESS: 14 | let result = _.assign({}, state, {list: [ 15 | ...state.list, 16 | _.assign({}, action.course) 17 | ]}); 18 | 19 | return result; 20 | 21 | case types.UPDATE_COURSE_SUCCESS: 22 | return _.assign({}, state, {list: [ 23 | ...state.list.filter(course => course.id !== action.course.id), 24 | _.assign({}, action.course) 25 | ]}); 26 | 27 | case types.DELETE_COURSE_SUCCESS: 28 | return _.assign({}, state, {list: [ 29 | ...state.list.filter(course => course.id !== action.id) 30 | ]}); 31 | 32 | default: 33 | return state; 34 | } 35 | } -------------------------------------------------------------------------------- /client/src/reducers/departmentReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | import _ from 'lodash'; 4 | 5 | export default function departmentReducer(state = initialState.department, action) { 6 | switch (action.type) { 7 | case types.LOAD_DEPARTMENTS_SUCCESS: 8 | return _.assign({}, state, {list: action.departments}); 9 | 10 | case types.LOAD_DEPARTMENT_SUCCESS: 11 | return _.assign({}, state, {current: action.department}); 12 | 13 | case types.CREATE_DEPARTMENT_SUCCESS: 14 | let result = _.assign({}, state, {list: [ 15 | ...state.list, 16 | _.assign({}, action.department) 17 | ]}); 18 | 19 | return result; 20 | 21 | case types.UPDATE_DEPARTMENT_SUCCESS: 22 | return _.assign({}, state, {list: [ 23 | ...state.list.filter(department => department.id !== action.department.id), 24 | _.assign({}, action.department) 25 | ]}); 26 | 27 | case types.DELETE_DEPARTMENT_SUCCESS: 28 | return _.assign({}, state, {list: [ 29 | ...state.list.filter(department => department.id !== action.id) 30 | ]}); 31 | 32 | default: 33 | return state; 34 | } 35 | } -------------------------------------------------------------------------------- /client/src/reducers/enrollmentReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | import _ from 'lodash'; 4 | 5 | export default function enrollmentReducer(state = initialState.enrollment, action) { 6 | switch (action.type) { 7 | case types.LOAD_ENROLLMENTS_SUCCESS: 8 | return _.assign({}, state, {list: action.enrollments}); 9 | 10 | default: 11 | return state; 12 | } 13 | } -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import ajaxCallsInProgress from './ajaxStatusReducer'; 3 | import student from './studentReducer'; 4 | import department from './departmentReducer'; 5 | import instructor from './instructorReducer'; 6 | import course from './courseReducer'; 7 | import enrollment from './enrollmentReducer'; 8 | 9 | const rootReducer = combineReducers({ 10 | student, 11 | department, 12 | instructor, 13 | course, 14 | enrollment, 15 | ajaxCallsInProgress, 16 | }); 17 | 18 | export default rootReducer; 19 | -------------------------------------------------------------------------------- /client/src/reducers/initialState.js: -------------------------------------------------------------------------------- 1 | export default { 2 | student: { 3 | list: [], 4 | current: {}, 5 | statisticsList: [], 6 | totalCount: 0 7 | }, 8 | department: { 9 | list: [], 10 | current: {} 11 | }, 12 | instructor: { 13 | list: [], 14 | current: {} 15 | }, 16 | course: { 17 | list: [], 18 | current: {} 19 | }, 20 | enrollment: { 21 | list: [] 22 | }, 23 | ajaxCallsInProgress: 0 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/reducers/instructorReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | import _ from 'lodash'; 4 | 5 | export default function instructorReducer(state = initialState.instructor, action) { 6 | switch (action.type) { 7 | case types.LOAD_INSTRUCTORS_SUCCESS: 8 | return _.assign({}, state, {list: action.instructors}); 9 | 10 | case types.LOAD_INSTRUCTOR_SUCCESS: 11 | return _.assign({}, state, {current: action.instructor}); 12 | 13 | case types.CREATE_INSTRUCTOR_SUCCESS: 14 | let result = _.assign({}, state, {list: [ 15 | ...state.list, 16 | _.assign({}, action.instructor) 17 | ]}); 18 | 19 | return result; 20 | 21 | case types.UPDATE_INSTRUCTOR_SUCCESS: 22 | return _.assign({}, state, {list: [ 23 | ...state.list.filter(instructor => instructor.id !== action.instructor.id), 24 | _.assign({}, action.instructor) 25 | ]}); 26 | 27 | case types.DELETE_INSTRUCTOR_SUCCESS: 28 | return _.assign({}, state, {list: [ 29 | ...state.list.filter(instructor => instructor.id !== action.id) 30 | ]}); 31 | 32 | default: 33 | return state; 34 | } 35 | } -------------------------------------------------------------------------------- /client/src/reducers/studentReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | import initialState from './initialState'; 3 | import _ from 'lodash'; 4 | 5 | export default function studentReducer(state = initialState.student, action) { 6 | switch (action.type) { 7 | case types.LOAD_STUDENTS_STATISTICS_SUCCESS: 8 | return _.assign({}, state, {statisticsList: action.statistics}); 9 | 10 | case types.LOAD_STUDENTS_SUCCESS: 11 | return _.assign({}, state, {list: action.students}); 12 | 13 | case types.COUNT_STUDENTS_SUCCCESS: 14 | return _.assign({}, state, {totalCount: action.count}); 15 | 16 | case types.LOAD_STUDENT_SUCCESS: 17 | return _.assign({}, state, {current: action.student}); 18 | 19 | case types.CREATE_STUDENT_SUCCESS: 20 | let result = _.assign({}, state, {list: [ 21 | ...state.list, 22 | _.assign({}, action.student) 23 | ]}); 24 | 25 | return result; 26 | 27 | case types.UPDATE_STUDENT_SUCCESS: 28 | return _.assign({}, state, {list: [ 29 | ...state.list.filter(student => student.id !== action.student.id), 30 | _.assign({}, action.student) 31 | ]}); 32 | 33 | case types.DELETE_STUDENT_SUCCESS: 34 | return _.assign({}, state, {list: [ 35 | ...state.list.filter(student => student.id !== action.id) 36 | ]}); 37 | 38 | default: 39 | return state; 40 | } 41 | } -------------------------------------------------------------------------------- /client/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, IndexRoute} from 'react-router'; 3 | import App from './components/AppContainer'; 4 | import HomePage from './components/HomePage'; 5 | import AboutPage from './components/AboutPage'; 6 | import CoursesPage from './components/CoursesPage'; 7 | import StudentsPage from './components/StudentsPage'; 8 | import InstructorsPage from './components/InstructorsPage'; 9 | import DepartmentsPage from './components/DepartmentsPage'; 10 | import NotFountPage from './components/NotFoundPage'; 11 | 12 | export default ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /client/src/services/courseService.js: -------------------------------------------------------------------------------- 1 | import httpHelper from '../helpers/httpHelper'; 2 | 3 | export default { 4 | getCourses, 5 | getCourse, 6 | saveCourse, 7 | deleteCourse 8 | }; 9 | 10 | function getCourses(departmentId) { 11 | return httpHelper.get('/api/course/list', {departmentId}); 12 | } 13 | 14 | function getCourse(id) { 15 | return httpHelper.get('/api/course/getCourse', {id}); 16 | } 17 | 18 | function saveCourse(course) { 19 | return httpHelper.post('/api/course/save', {course}); 20 | } 21 | 22 | function deleteCourse(id) { 23 | return httpHelper.delete('/api/course/delete', {id}); 24 | } -------------------------------------------------------------------------------- /client/src/services/departmentService.js: -------------------------------------------------------------------------------- 1 | import httpHelper from '../helpers/httpHelper'; 2 | 3 | export default { 4 | getDepartments, 5 | getDepartment, 6 | saveDepartment, 7 | deleteDepartment 8 | }; 9 | 10 | function getDepartments() { 11 | return httpHelper.get('/api/department/list', {}); 12 | } 13 | 14 | function getDepartment(id) { 15 | return httpHelper.get('/api/department/getDepartment', {id}); 16 | } 17 | 18 | function saveDepartment(department) { 19 | return httpHelper.post('/api/department/save', {department}); 20 | } 21 | 22 | function deleteDepartment(id) { 23 | return httpHelper.delete('/api/department/delete', {id}); 24 | } -------------------------------------------------------------------------------- /client/src/services/enrollmentService.js: -------------------------------------------------------------------------------- 1 | import httpHelper from '../helpers/httpHelper'; 2 | 3 | export default { 4 | getEnrollments 5 | }; 6 | 7 | function getEnrollments(courseId) { 8 | return httpHelper.get('/api/enrollment/list', {courseId}); 9 | } -------------------------------------------------------------------------------- /client/src/services/instructorService.js: -------------------------------------------------------------------------------- 1 | import httpHelper from '../helpers/httpHelper'; 2 | 3 | export default { 4 | getInstructors, 5 | getInstructor, 6 | saveInstructor, 7 | deleteInstructor 8 | }; 9 | 10 | function getInstructors() { 11 | return httpHelper.get('/api/instructor/list', {}); 12 | } 13 | 14 | function getInstructor(id) { 15 | return httpHelper.get('/api/instructor/getInstructor', {id}); 16 | } 17 | 18 | function saveInstructor(instructor) { 19 | return httpHelper.post('/api/instructor/save', {instructor}); 20 | } 21 | 22 | function deleteInstructor(id) { 23 | return httpHelper.delete('/api/instructor/delete', {id}); 24 | } -------------------------------------------------------------------------------- /client/src/services/studentService.js: -------------------------------------------------------------------------------- 1 | import httpHelper from '../helpers/httpHelper'; 2 | 3 | export default { 4 | getStudentsStatistics, 5 | getStudents, 6 | getStudent, 7 | saveStudent, 8 | deleteStudent 9 | }; 10 | 11 | function getStudentsStatistics() { 12 | return httpHelper.get('/api/student/statistics', {}); 13 | } 14 | 15 | function getStudents(search, sortOrder, pageNumber, pageSize) { 16 | let data = { 17 | search, 18 | sortOrder, 19 | pageNumber, 20 | pageSize 21 | }; 22 | 23 | return httpHelper.get('/api/student/list', data); 24 | } 25 | 26 | function getStudent(id) { 27 | return httpHelper.get('/api/student/getStudent', {id}); 28 | } 29 | 30 | function saveStudent(student) { 31 | return httpHelper.post('/api/student/save', {student}); 32 | } 33 | 34 | function deleteStudent(id) { 35 | return httpHelper.delete('/api/student/delete', {id}); 36 | } -------------------------------------------------------------------------------- /client/src/store/configureStore.dev.js: -------------------------------------------------------------------------------- 1 | import rootReducer from '../reducers'; 2 | const redux = require('redux'); 3 | const reduxImmutableStateInvariant = require('redux-immutable-state-invariant'); 4 | const thunk = require('redux-thunk'); 5 | 6 | export default function configureStore(initialState) { 7 | return redux.createStore( 8 | rootReducer, 9 | initialState, 10 | redux.compose( 11 | redux.applyMiddleware(thunk.default, reduxImmutableStateInvariant()), 12 | window.devToolsExtension ? window.devToolsExtension() : f => f 13 | ) 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import configureStoreProd from './configureStore.prod'; 2 | import configureStoreDev from './configureStore.dev'; 3 | import config from '../helpers/clientConfig'; 4 | 5 | let configure = null; 6 | 7 | if (config.isDevLocal) { 8 | configure = configureStoreDev; 9 | } else { 10 | configure = configureStoreProd; 11 | } 12 | 13 | export default configure; 14 | -------------------------------------------------------------------------------- /client/src/store/configureStore.prod.js: -------------------------------------------------------------------------------- 1 | const redux = require('redux'); 2 | const thunk = require('redux-thunk'); 3 | import rootReducer from '../reducers'; 4 | 5 | export default function configureStore(initialState) { 6 | return redux.createStore( 7 | rootReducer, 8 | initialState, 9 | redux.applyMiddleware(thunk.default) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/styles/app.css: -------------------------------------------------------------------------------- 1 | #page-body { 2 | position: absolute; 3 | top: 53px; 4 | left: 0; 5 | right: 0; 6 | bottom: 0; 7 | } 8 | 9 | .tools a { 10 | color: #000; 11 | padding: 0 6px; 12 | } 13 | 14 | .form-label { 15 | text-align: right; 16 | } -------------------------------------------------------------------------------- /client/tasks/copyAssets.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var path = require('path'); 3 | var Promise = require('bluebird'); 4 | var resources = require('../public/vendor.json').staticResources; 5 | var _ = require('lodash'); 6 | 7 | try { 8 | _.forEach(resources, (item) => { 9 | fs.copySync(item.from, path.join('public/static', item.to)); 10 | console.log("Copied: " + item.from); 11 | }); 12 | } catch (err) { 13 | console.log(err); 14 | } 15 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | from client folder: mklink /J .\typings .\typings -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contoso-express", 3 | "version": "0.1.0", 4 | "description": "Full-stack JavaScript application based on Contoso University .NET tutorial", 5 | "keywords": [ 6 | "TypeScript", 7 | "Contoso", 8 | "Express", 9 | "Postgres", 10 | "Passport", 11 | "React" 12 | ], 13 | "main": "index.js", 14 | "scripts": { 15 | "postinstall": "cd server && npm install && typings install && cd ../client && npm install", 16 | "start": "cd server && npm run start", 17 | "watch": "cd client && npm run start", 18 | "build": "cd server && npm run build && cd ../client && npm run copy-assets && npm run build" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/yegor-sytnyk/contoso-express.git" 23 | }, 24 | "author": "", 25 | "license": "ISC", 26 | "dependencies": {}, 27 | "devDependencies": {} 28 | } 29 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | typings/globals/ 4 | typings/modules/ 5 | .idea/ 6 | data/db/ 7 | src/test.ts 8 | data/logs 9 | data/config/local.json 10 | .vscode/ 11 | -------------------------------------------------------------------------------- /server/data/config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "appName": "Contoso Express", 4 | "isDevLocal": true, 5 | "logErrors": true, 6 | "rootUrl": "http://localhost:3500" 7 | }, 8 | "db": { 9 | "host": "localhost", 10 | "dbName": "contoso", 11 | "username": "admin", 12 | "password": "password123" 13 | }, 14 | "web": { 15 | "port": 3500, 16 | "sessionSecret": "secret_value_!?!" 17 | }, 18 | "email": { 19 | "fromNoReply": "noreply@contoso.com" 20 | }, 21 | "auth": { 22 | "useAuth": true, 23 | "google": { 24 | "clientID": "???", 25 | "clientSecret": "???" 26 | }, 27 | "facebook": { 28 | "clientID": "???", 29 | "clientSecret": "???" 30 | } 31 | }, 32 | "format": { 33 | "date": "MM/DD/YYYY", 34 | "year": "YYYY", 35 | "currencySymbol": "$" 36 | } 37 | } -------------------------------------------------------------------------------- /server/data/emails/activation/html.hbs: -------------------------------------------------------------------------------- 1 |

2 | Activation token: {{siteRootUrl}}/activate/{{token}} 3 |

-------------------------------------------------------------------------------- /server/data/emails/activation/subject.hbs: -------------------------------------------------------------------------------- 1 | Activation Email -------------------------------------------------------------------------------- /server/data/emails/password_reset/html.hbs: -------------------------------------------------------------------------------- 1 |

2 | Reset password: {{siteRootUrl}}/passwordReset/{{token}} 3 |

-------------------------------------------------------------------------------- /server/data/emails/password_reset/subject.hbs: -------------------------------------------------------------------------------- 1 | Reset Password Email -------------------------------------------------------------------------------- /server/data/text/textValues.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": { 3 | "app": { 4 | "error": "Server error", 5 | "request_validation": "Request validation failed: ${validationMessage}", 6 | "department_not_found": "Department not found.", 7 | "course_not_found": "Course not found.", 8 | "instructor_not_found": "Instructor not found.", 9 | "student_not_found": "Student not found." 10 | }, 11 | "auth": { 12 | "email_taken": "This email is already registered.", 13 | "already_logged_in": "Log out before signing up.", 14 | "user_not_found": "User not found.", 15 | "wrong_password": "Wrong password.", 16 | "account_not_activated": "Your account is not activated yet. Please check your email for activation letter or sign up again to get a new one.", 17 | "email_activated": "This email is already activated.", 18 | "activation:no_token": "No activation token provided.", 19 | "activation:wrong_token": "Wrong activation token.", 20 | "activation:expired_token": "Activation token has expired. New activation email was send.", 21 | "reset_password:no_token": "No reset token provided.", 22 | "reset_password:wrong_token": "Wrong reset password token.", 23 | "reset_password:token_expired": "Reset password token has expired. New activation email was send.", 24 | "forgot_password:no_email": "There is no user with provided email." 25 | } 26 | }, 27 | "info": { 28 | "auth": { 29 | "activation_email_confirmation": "Activation email was send. Please, check you inbox.", 30 | "reset_password_email_confirmation": "We've just dropped you an email. Please check your mail to reset your password. Thanks!", 31 | "reset_password_success": "Your password was reset successfully.", 32 | "activation_success": "Activation was successful! Please, login." 33 | } 34 | }, 35 | "warning": { 36 | "auth": { 37 | "required_field": "Please fill the ${name} field.", 38 | "email_not_valid": "Email is not valid.", 39 | "passwords_not_match": "Passwords do not match.", 40 | "password_length": "Your password must be 8-16 characters." 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "tsc && node build/startServer", 7 | "seed": "node build/tasks/syncDb", 8 | "build": "tsc" 9 | }, 10 | "dependencies": { 11 | "bcrypt-nodejs": "0.0.3", 12 | "bluebird": "^3.4.0", 13 | "body-parser": "^1.15.1", 14 | "compression": "^1.6.2", 15 | "config": "^1.21.0", 16 | "connect-flash": "^0.1.1", 17 | "cookie-parser": "^1.4.3", 18 | "cookie-session": "^2.0.0-alpha.1", 19 | "cors": "^2.8.1", 20 | "crypto": "0.0.3", 21 | "email-templates": "^2.3.0", 22 | "entities": "^1.1.1", 23 | "es6-template-strings": "^2.0.0", 24 | "express": "^4.13.4", 25 | "express-hbs": "^1.0.1", 26 | "handlebars": "^4.0.5", 27 | "joi": "^8.4.2", 28 | "lodash": "^4.13.1", 29 | "moment": "^2.14.1", 30 | "morgan": "^1.7.0", 31 | "nodemailer": "^2.4.2", 32 | "passport": "^0.3.2", 33 | "passport-facebook": "^2.1.1", 34 | "passport-google-oauth": "^1.0.0", 35 | "passport-local": "^1.0.0", 36 | "pg": "^6.0.1", 37 | "pg-hstore": "^2.3.2", 38 | "rimraf": "^2.5.3", 39 | "sequelize": "^3.23.3", 40 | "winston": "^2.2.0" 41 | }, 42 | "devDependencies": { 43 | "chai": "^3.5.0", 44 | "mocha": "^2.5.3", 45 | "tslint": "^3.15.1", 46 | "typescript": "^1.8.10" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/appError.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | interface IAppError { 4 | uiShow: boolean; 5 | log: boolean; 6 | type: string; 7 | code: string; 8 | data: Object; 9 | message: string; 10 | isAppError: boolean; 11 | } 12 | 13 | interface AppErrorOptions { 14 | uiShow: boolean, 15 | log: boolean, 16 | data: Object 17 | } 18 | 19 | export class AppError implements AppError { 20 | message = ''; 21 | uiShow = true; 22 | log = false; 23 | type = ''; 24 | code = ''; 25 | data: Object; 26 | isAppError = true; 27 | 28 | constructor(message: string, options?: Object); 29 | constructor(type: string, code: string, options?: Object); 30 | 31 | constructor(...args: any[]) { 32 | Error.captureStackTrace(this, this.constructor); 33 | 34 | 35 | //signature type, code, options 36 | if (_.isString(args[0]) && _.isString(args[1])) { 37 | this.type = args[0]; 38 | this.code = args[1]; 39 | _.merge(this, args[2]); 40 | } 41 | //signature message, options 42 | else if (_.isString(args[0])) { 43 | this.message = args[0]; 44 | _.merge(this, args[1]); 45 | } else { 46 | throw new Error('Unsupported AppError signature'); 47 | } 48 | } 49 | } 50 | 51 | export default AppError; -------------------------------------------------------------------------------- /server/src/auth/authHelper.ts: -------------------------------------------------------------------------------- 1 | import config from '../config'; 2 | import emailHelper from '../helpers/emailHelper'; 3 | import errorHelper from '../helpers/errorHelper'; 4 | import AppError from '../appError'; 5 | import textValue from '../helpers/textValueHelper'; 6 | import controllerHelper from '../controllers/_controllerHelper'; 7 | 8 | export default { 9 | renderView, 10 | setStatusMessage, 11 | handleError, 12 | redirectToLogIn, 13 | sendAuthErrorMessage, 14 | sendAuthMessage, 15 | sendResetPasswordEmail, 16 | sendActivationEmail, 17 | isValidEmail, 18 | isValidPassword 19 | }; 20 | 21 | function renderView(viewName, viewModel, req, res) { 22 | let statusMessage = null; 23 | 24 | let flashArray = req.flash('statusMessage'); 25 | if (flashArray && flashArray.length > 0) { 26 | statusMessage = flashArray[0]; 27 | } 28 | 29 | if (statusMessage) { 30 | viewModel.statusMessage = statusMessage; 31 | } 32 | 33 | return controllerHelper.renderView(viewName, viewModel, res); 34 | } 35 | 36 | function setStatusMessage(req, message: string, type?: string) { 37 | if (!type) { 38 | type = 'error'; 39 | } 40 | 41 | let uiClass = ''; 42 | 43 | switch (type) { 44 | case 'error': 45 | uiClass = 'alert-danger'; 46 | break; 47 | case 'success': 48 | uiClass = 'alert-success'; 49 | break; 50 | case 'info': 51 | uiClass = 'alert-info'; 52 | break; 53 | case 'warning': 54 | uiClass = 'alert-warning'; 55 | break; 56 | default: 57 | throw new AppError(`Not supported status message type: '${type}'`); 58 | } 59 | 60 | let statusMessage = { 61 | message: message, 62 | uiClass: uiClass 63 | }; 64 | 65 | req.flash('statusMessage', statusMessage); 66 | } 67 | 68 | function redirectToLogIn(message, type, req, res) { 69 | setStatusMessage(req, message, type); 70 | res.redirect('/login'); 71 | } 72 | 73 | function sendAuthErrorMessage(message, done, req) { 74 | return done(null, false, setStatusMessage(req, message)); 75 | } 76 | 77 | function sendAuthMessage(message, type, done, req) { 78 | if (message) return done(null, false, setStatusMessage(req, message, type)); 79 | 80 | return done(); 81 | } 82 | 83 | function sendResetPasswordEmail(email, token) { 84 | let data = { 85 | token, 86 | siteRootUrl: config.app.rootUrl 87 | }; 88 | 89 | return emailHelper.sendEmailTemplate('password_reset', data, { 90 | to: email, 91 | from: config.email.fromNoReply 92 | }); 93 | } 94 | 95 | function sendActivationEmail(email, token) { 96 | let data = { 97 | token, 98 | siteRootUrl: config.app.rootUrl 99 | }; 100 | 101 | return emailHelper.sendEmailTemplate('activation', data, { 102 | to: email, 103 | from: config.email.fromNoReply 104 | }); 105 | } 106 | 107 | function isValidEmail(email) { 108 | const re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; 109 | return re.test(email); 110 | } 111 | 112 | function isValidPassword(password) { 113 | let message = ''; 114 | const minMaxLength = /^[\s\S]{8,16}$/, 115 | upper = /[A-Z]/, 116 | lower = /[a-z]/, 117 | hasNumber = /[0-9]/, 118 | hasSpecial = /[ !'#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/; 119 | 120 | if (!minMaxLength.test(password)) { 121 | message = textValue.warning('auth', 'password_length'); 122 | return message; 123 | } 124 | 125 | //NOTE add additional password rules if required 126 | 127 | return message; 128 | } 129 | 130 | function handleError(error) { 131 | errorHelper.logError(error); 132 | 133 | let errorMessage = errorHelper.getErrorMessage(error); 134 | 135 | if (!errorMessage) return 'Auth Error'; //Cannot find error description 136 | 137 | return errorMessage; 138 | } -------------------------------------------------------------------------------- /server/src/auth/authInit.ts: -------------------------------------------------------------------------------- 1 | import userRepository from '../repositories/userRepository'; 2 | import googleAuth from './providers/googleAuth'; 3 | import facebookAuth from './providers/facebookAuth'; 4 | import authControllerInit from './authController'; 5 | 6 | export default initPassport; 7 | 8 | function initPassport(passport) { 9 | authControllerInit(passport); 10 | 11 | // passport session setup 12 | // used to serialize the user for the session 13 | passport.serializeUser(function (user, done) { 14 | done(null, user.id); 15 | }); 16 | 17 | // used to deserialize the user 18 | passport.deserializeUser(function (id, done) { 19 | userRepository.getById(id) 20 | .then((user) => { 21 | done(null, user); 22 | }) 23 | .catch((err) => { 24 | done(err, null); 25 | }); 26 | }); 27 | 28 | googleAuth.init(passport); 29 | facebookAuth.init(passport); 30 | } -------------------------------------------------------------------------------- /server/src/auth/providers/facebookAuth.ts: -------------------------------------------------------------------------------- 1 | import {Strategy} from 'passport-facebook'; 2 | import config from '../../config'; 3 | import userRepository from '../../repositories/userRepository'; 4 | 5 | export default { 6 | init 7 | }; 8 | 9 | let providerName = 'facebook'; 10 | 11 | let strategySettings = { 12 | clientID: config.auth.facebook.clientID, 13 | clientSecret: config.auth.facebook.clientSecret, 14 | callbackURL: `${config.app.rootUrl}/auth/${providerName}/callback`, 15 | profileFields: ['id', 'emails', 'name'] 16 | }; 17 | 18 | function init(passport) { 19 | passport.use(providerName, new Strategy(strategySettings, providerLogin)); 20 | } 21 | 22 | async function providerLogin(token, refreshToken, profile, done) { 23 | try { 24 | let providerUser = await userRepository.findUserByAuthProviderId(profile.id, providerName); 25 | 26 | if (providerUser) { 27 | return done(null, providerUser); 28 | } 29 | 30 | let email = (profile.emails[0].value || '').toLowerCase(); 31 | 32 | let user = await userRepository.findUserWithEmail(email); 33 | 34 | let profileData = { 35 | token, 36 | email, 37 | id: profile.id, 38 | name: profile.displayName 39 | }; 40 | 41 | user = await userRepository.saveAuthProviderProfile(user, profileData, providerName); 42 | 43 | return done(null, user); 44 | 45 | } catch (err) { 46 | return done(err, null); 47 | } 48 | } -------------------------------------------------------------------------------- /server/src/auth/providers/googleAuth.ts: -------------------------------------------------------------------------------- 1 | import {OAuth2Strategy as Strategy} from 'passport-google-oauth'; 2 | import config from '../../config'; 3 | import userRepository from '../../repositories/userRepository'; 4 | 5 | export default { 6 | init 7 | }; 8 | 9 | let providerName = 'google'; 10 | 11 | let strategySettings = { 12 | clientID: config.auth.google.clientID, 13 | clientSecret: config.auth.google.clientSecret, 14 | callbackURL: `${config.app.rootUrl}/auth/${providerName}/callback` 15 | }; 16 | 17 | function init(passport) { 18 | passport.use(providerName, new Strategy(strategySettings, providerLogin)); 19 | } 20 | 21 | async function providerLogin(token, refreshToken, profile, done) { 22 | try { 23 | let providerUser = await userRepository.findUserByAuthProviderId(profile.id, providerName); 24 | 25 | if (providerUser) { 26 | return done(null, providerUser); 27 | } 28 | 29 | let email = (profile.emails[0].value || '').toLowerCase(); 30 | 31 | let user = await userRepository.findUserWithEmail(email); 32 | 33 | let profileData = { 34 | token, 35 | email, 36 | id: profile.id, 37 | name: profile.displayName 38 | }; 39 | 40 | user = await userRepository.saveAuthProviderProfile(user, profileData, providerName); 41 | 42 | return done(null, user); 43 | 44 | } catch (err) { 45 | return done(err, null); 46 | } 47 | } -------------------------------------------------------------------------------- /server/src/config.ts: -------------------------------------------------------------------------------- 1 | import pathHelper from './helpers/pathHelper'; 2 | 3 | interface IConfigValues { 4 | app: { 5 | appName: string, 6 | isDevLocal: boolean, 7 | logErrors: boolean, 8 | rootUrl: string 9 | }, 10 | db: { 11 | host: string, 12 | dbName: string, 13 | username: string, 14 | password: string 15 | }, 16 | web: { 17 | port: number, 18 | sessionSecret: string 19 | }, 20 | email: { 21 | fromNoReply: string 22 | }, 23 | auth: { 24 | useAuth: boolean, 25 | google: { 26 | clientID: string, 27 | clientSecret: string 28 | }, 29 | facebook: { 30 | clientID: string, 31 | clientSecret: string 32 | } 33 | }, 34 | format: { 35 | date: string, 36 | year: string, 37 | currencySymbol: string 38 | } 39 | } 40 | 41 | let configValues = {}; 42 | 43 | ensureConfigPath(); 44 | 45 | const configReader = require('config'); 46 | 47 | loadConfig(); 48 | 49 | export function loadConfig() { 50 | 51 | (configValues).app = {}; 52 | configValues.app.appName = get('app.appName'); 53 | configValues.app.isDevLocal = get('app.isDevLocal'); 54 | configValues.app.logErrors = get('app.logErrors'); 55 | configValues.app.rootUrl = get('app.rootUrl'); 56 | 57 | (configValues).db = {}; 58 | configValues.db.host = get('db.host'); 59 | configValues.db.dbName = get('db.dbName'); 60 | configValues.db.username = get('db.username'); 61 | configValues.db.password = get('db.password'); 62 | 63 | (configValues).web = {}; 64 | configValues.web.port = get('web.port'); 65 | configValues.web.sessionSecret = get('web.sessionSecret'); 66 | 67 | (configValues).email = {}; 68 | configValues.email.fromNoReply = get('email.fromNoReply'); 69 | 70 | (configValues).auth = {}; 71 | configValues.auth.useAuth = get('auth.useAuth'); 72 | 73 | (configValues).auth.google = {}; 74 | configValues.auth.google.clientID = get('auth.google.clientID'); 75 | configValues.auth.google.clientSecret = get('auth.google.clientSecret'); 76 | 77 | (configValues).auth.facebook = {}; 78 | configValues.auth.facebook.clientID = get('auth.facebook.clientID'); 79 | configValues.auth.facebook.clientSecret = get('auth.facebook.clientSecret'); 80 | 81 | (configValues).format = {}; 82 | configValues.format.date = get('format.date'); 83 | configValues.format.year = get('format.year'); 84 | configValues.format.currencySymbol = get('format.currencySymbol'); 85 | } 86 | 87 | function get(key) { 88 | return configReader.get(key); 89 | } 90 | 91 | function ensureConfigPath() { 92 | if (!process.env['NODE_CONFIG_DIR']) { 93 | let configPath = pathHelper.getDataRelative('config'); 94 | process.env['NODE_CONFIG_DIR'] = configPath; 95 | } 96 | } 97 | 98 | export default configValues; -------------------------------------------------------------------------------- /server/src/controllers/_controllerHelper.ts: -------------------------------------------------------------------------------- 1 | import errorHelper from '../helpers/errorHelper'; 2 | import AppError from '../appError'; 3 | import {Response} from 'express'; 4 | import * as Joi from 'joi'; 5 | 6 | export default { 7 | sendFailureMessage, 8 | sendSuccessMessage, 9 | sendData, 10 | renderView, 11 | loadSchema 12 | }; 13 | 14 | function sendFailureMessage(error, res) { 15 | errorHelper.logError(error); 16 | let errorMessage = errorHelper.getErrorMessage(error); 17 | 18 | res.send({'status': 'failure', message: errorMessage}); 19 | } 20 | 21 | function sendSuccessMessage(message, res) { 22 | res.send({status: 'success', 'message': message}); 23 | } 24 | 25 | function sendData(data, res) { 26 | data.status = 'success'; 27 | res.send(data); 28 | } 29 | 30 | function renderView(viewName: string, data, res) { 31 | if (!data) data = {}; 32 | 33 | res.render(viewName, data); 34 | } 35 | 36 | function loadSchema(data, schema): Promise { 37 | let validationOptions = { 38 | stripUnknown: true 39 | }; 40 | 41 | return new Promise(function (resolve, reject) { 42 | Joi.validate(data, schema, validationOptions, function (err, val) { 43 | if (!err) return resolve(val); 44 | 45 | let error = null; 46 | 47 | if (err.name !== 'ValidationError') { 48 | error = new Error('Unsupported Validation Error'); 49 | return reject(err); 50 | } 51 | 52 | let validationMessage = err.details[0].message; 53 | 54 | error = new AppError('app', 'request_validation', { 55 | data: {validationMessage} 56 | }); 57 | 58 | return reject(error); 59 | }); 60 | }); 61 | } -------------------------------------------------------------------------------- /server/src/controllers/courseController.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import helper from './_controllerHelper'; 3 | import courseRepository from '../repositories/courseRepository'; 4 | 5 | export default { 6 | getCourses, 7 | getCourse, 8 | saveCourse, 9 | deleteCourse 10 | }; 11 | 12 | async function getCourses(req, res) { 13 | try { 14 | let departmentId = req.query.departmentId; 15 | 16 | let courses = await courseRepository.getCourses(departmentId); 17 | 18 | return helper.sendData({data: courses}, res); 19 | } catch (err) { 20 | helper.sendFailureMessage(err, res); 21 | } 22 | } 23 | 24 | async function getCourse(req, res) { 25 | try { 26 | let id = req.query.id; 27 | 28 | let course = await courseRepository.getCourseById(id); 29 | 30 | return helper.sendData({data: course}, res); 31 | } catch (err) { 32 | helper.sendFailureMessage(err, res); 33 | } 34 | } 35 | 36 | async function saveCourse(req, res) { 37 | try { 38 | let courseData = req.body.course; 39 | 40 | let schema = { 41 | id: Joi.number(), 42 | number: Joi.number().required(), 43 | title: Joi.string().required(), 44 | credits: Joi.number().required(), 45 | departmentId: Joi.number().required() 46 | }; 47 | 48 | let result = null; 49 | 50 | let course = await helper.loadSchema(courseData, schema); 51 | 52 | if (course.id) { 53 | result = await courseRepository.updateCourse(courseData); 54 | } else { 55 | result = await courseRepository.addCourse(courseData); 56 | } 57 | 58 | course = await courseRepository.getCourseById(result.id); 59 | 60 | return helper.sendData({data: course}, res); 61 | } catch (err) { 62 | helper.sendFailureMessage(err, res); 63 | } 64 | } 65 | 66 | async function deleteCourse(req, res) { 67 | try { 68 | let id = req.body.id; 69 | 70 | await courseRepository.deleteCourse(id); 71 | 72 | return helper.sendData({}, res); 73 | } catch (err) { 74 | helper.sendFailureMessage(err, res); 75 | } 76 | } -------------------------------------------------------------------------------- /server/src/controllers/departmentController.ts: -------------------------------------------------------------------------------- 1 | import helper from './_controllerHelper'; 2 | import departmentRepository from '../repositories/departmentRepository'; 3 | import * as Joi from 'joi'; 4 | import config from '../config'; 5 | 6 | export default { 7 | getDepartments, 8 | getDepartment, 9 | saveDepartment, 10 | deleteDepartment 11 | }; 12 | 13 | async function getDepartments(req, res) { 14 | try { 15 | let departments = await departmentRepository.getDepartments(); 16 | 17 | return helper.sendData({data: departments}, res); 18 | } catch (err) { 19 | helper.sendFailureMessage(err, res); 20 | } 21 | } 22 | 23 | async function getDepartment(req, res) { 24 | try { 25 | let id = req.query.id; 26 | 27 | let department = await departmentRepository.getDepartmentById(id); 28 | 29 | return helper.sendData({data: department}, res); 30 | } catch (err) { 31 | helper.sendFailureMessage(err, res); 32 | } 33 | } 34 | 35 | async function saveDepartment(req, res) { 36 | try { 37 | let data = req.body.department; 38 | 39 | // check if budget is empty 40 | if (!data.budget) data.budget = 0; 41 | 42 | let schema = { 43 | id: Joi.number(), 44 | name: Joi.string().required(), 45 | budget: Joi.number().required(), 46 | startDate: Joi.date(), 47 | instructorId: Joi.number().required() 48 | }; 49 | 50 | let result = null; 51 | 52 | let department = await helper.loadSchema(data, schema); 53 | 54 | if (department.id) { 55 | result = await departmentRepository.updateDepartment(department); 56 | } else { 57 | result = await departmentRepository.addDepartment(department); 58 | } 59 | 60 | department = await departmentRepository.getDepartmentById(result.id); 61 | 62 | return helper.sendData({data: department}, res); 63 | } catch (err) { 64 | helper.sendFailureMessage(err, res); 65 | } 66 | } 67 | 68 | async function deleteDepartment(req, res) { 69 | try { 70 | let id = req.body.id; 71 | 72 | await departmentRepository.deleteDepartment(id); 73 | 74 | return helper.sendData({}, res); 75 | } catch (err) { 76 | helper.sendFailureMessage(err, res); 77 | } 78 | } -------------------------------------------------------------------------------- /server/src/controllers/enrollmentController.ts: -------------------------------------------------------------------------------- 1 | import helper from './_controllerHelper'; 2 | import enrollmentRepository from '../repositories/enrollmentRepository'; 3 | 4 | export default { 5 | getEnrollmentsByCourse 6 | }; 7 | 8 | async function getEnrollmentsByCourse(req, res) { 9 | try { 10 | let courseId = req.query.courseId; 11 | 12 | let enrollments = await enrollmentRepository.getEnrollmentsByCourseId(courseId); 13 | 14 | return helper.sendData({data: enrollments}, res); 15 | } catch (err) { 16 | helper.sendFailureMessage(err, res); 17 | } 18 | } -------------------------------------------------------------------------------- /server/src/controllers/homeController.ts: -------------------------------------------------------------------------------- 1 | import helper from './_controllerHelper'; 2 | import pathHelper from '../helpers/pathHelper'; 3 | 4 | export default { 5 | home 6 | }; 7 | 8 | async function home(req, res) { 9 | try { 10 | return res.sendFile(pathHelper.getRelative('../client/build/index.html')); 11 | } catch (err) { 12 | helper.sendFailureMessage(err, res); 13 | } 14 | } -------------------------------------------------------------------------------- /server/src/controllers/instructorController.ts: -------------------------------------------------------------------------------- 1 | import * as Joi from 'joi'; 2 | import config from '../config'; 3 | import helper from './_controllerHelper'; 4 | import instructorRepository from '../repositories/instructorRepository'; 5 | import officeAssignmentRepository from '../repositories/officeAssignmentRepository'; 6 | 7 | export default { 8 | getInstructors, 9 | getInstructor, 10 | saveInstructor, 11 | deleteInstructor 12 | }; 13 | 14 | async function getInstructors(req, res) { 15 | try { 16 | let instructors = await instructorRepository.getInstructors(); 17 | 18 | return helper.sendData({data: instructors}, res); 19 | } catch (err) { 20 | helper.sendFailureMessage(err, res); 21 | } 22 | } 23 | 24 | async function getInstructor(req, res) { 25 | try { 26 | let id = req.query.id; 27 | 28 | let instructor = await instructorRepository.getInstructorById(id); 29 | 30 | return helper.sendData({data: instructor}, res); 31 | } catch (err) { 32 | helper.sendFailureMessage(err, res); 33 | } 34 | } 35 | 36 | async function saveInstructor(req, res) { 37 | try { 38 | let data = req.body.instructor; 39 | 40 | let schema = { 41 | id: Joi.number(), 42 | firstName: Joi.string().required(), 43 | lastName: Joi.string().required(), 44 | hireDate: Joi.date(), 45 | courses: Joi.array().items( 46 | Joi.object().keys({ 47 | id: Joi.number().required() 48 | }) 49 | ), 50 | officeAssignment: Joi.object().keys({ 51 | id: Joi.number(), 52 | location: Joi.string().allow('') 53 | }) 54 | }; 55 | 56 | let result = null; 57 | 58 | let instructor = await helper.loadSchema(data, schema); 59 | 60 | if (instructor.id) { 61 | result = await instructorRepository.updateInstructor(instructor); 62 | } else { 63 | result = await instructorRepository.addInstructor(instructor); 64 | } 65 | 66 | await officeAssignmentRepository.saveOfficeAssignment(instructor.officeAssignment, result.id); 67 | 68 | instructor = await instructorRepository.getInstructorById(result.id); 69 | 70 | return helper.sendData({data: instructor}, res); 71 | } catch (err) { 72 | helper.sendFailureMessage(err, res); 73 | } 74 | } 75 | 76 | async function deleteInstructor(req, res) { 77 | try { 78 | let id = req.body.id; 79 | 80 | await officeAssignmentRepository.deleteOfficeAssignmentByInstructorId(id); 81 | 82 | await instructorRepository.deleteInstructor(id); 83 | 84 | return helper.sendData({}, res); 85 | } catch (err) { 86 | helper.sendFailureMessage(err, res); 87 | } 88 | } -------------------------------------------------------------------------------- /server/src/controllers/studentController.ts: -------------------------------------------------------------------------------- 1 | import helper from './_controllerHelper'; 2 | import studentRepository from '../repositories/studentRepository'; 3 | import * as Joi from 'joi'; 4 | import config from '../config'; 5 | 6 | export default { 7 | getStudentsStatistics, 8 | getStudents, 9 | getStudent, 10 | saveStudent, 11 | deleteStudent 12 | }; 13 | 14 | async function getStudentsStatistics(req, res) { 15 | try { 16 | let studentStatistics = await studentRepository.getStudentStatistics(); 17 | 18 | return helper.sendData({data: studentStatistics}, res); 19 | } catch (err) { 20 | helper.sendFailureMessage(err, res); 21 | } 22 | } 23 | 24 | async function getStudents(req, res) { 25 | try { 26 | let search = req.query.search; 27 | let sortOrder = req.query.sortOrder; 28 | let pageNumber = req.query.pageNumber; 29 | let pageSize = req.query.pageSize; 30 | 31 | let result = await studentRepository.getStudents(search, sortOrder, pageNumber, pageSize); 32 | 33 | return helper.sendData({data: result}, res); 34 | } catch (err) { 35 | helper.sendFailureMessage(err, res); 36 | } 37 | } 38 | 39 | async function getStudent(req, res) { 40 | try { 41 | let id = req.query.id; 42 | 43 | let student = await studentRepository.getStudentById(id); 44 | 45 | return helper.sendData({data: student}, res); 46 | } catch (err) { 47 | helper.sendFailureMessage(err, res); 48 | } 49 | } 50 | 51 | async function saveStudent(req, res) { 52 | try { 53 | let data = req.body.student; 54 | 55 | let schema = { 56 | id: Joi.number(), 57 | firstName: Joi.string().required(), 58 | lastName: Joi.string().required(), 59 | enrollmentDate: Joi.date() 60 | }; 61 | 62 | let result = null; 63 | 64 | let student = await helper.loadSchema(data, schema); 65 | 66 | if (student.id) { 67 | result = await studentRepository.updateStudent(student); 68 | } else { 69 | result = await studentRepository.addStudent(student); 70 | } 71 | 72 | return helper.sendData({data: result}, res); 73 | } catch (err) { 74 | helper.sendFailureMessage(err, res); 75 | } 76 | } 77 | 78 | async function deleteStudent(req, res) { 79 | try { 80 | let id = req.body.id; 81 | 82 | await studentRepository.deleteStudent(id); 83 | 84 | return helper.sendData({}, res); 85 | } catch (err) { 86 | helper.sendFailureMessage(err, res); 87 | } 88 | } -------------------------------------------------------------------------------- /server/src/database/database.ts: -------------------------------------------------------------------------------- 1 | import {DbModels} from '../../typings/app/models'; 2 | import config from '../config'; 3 | const Sequelize = require('sequelize'); 4 | const models = require('./models/index'); 5 | 6 | interface Db { 7 | sequelize: any, 8 | models: DbModels 9 | } 10 | 11 | interface DbConnectionOptions { 12 | dbPath?: string, 13 | dbName?: string 14 | } 15 | 16 | export default { 17 | init: init 18 | }; 19 | 20 | function init(connectionOptions?: DbConnectionOptions): Db { 21 | const sequelize = getConnection(connectionOptions); 22 | const dbModels = models.init(sequelize); 23 | 24 | return { 25 | sequelize, 26 | models: dbModels 27 | }; 28 | } 29 | 30 | function getConnection(connectionOptions: DbConnectionOptions) { 31 | let options = { 32 | dialect: 'postgres', 33 | host: config.db.host, 34 | pool: { 35 | max: 5, 36 | min: 0, 37 | idle: 10000 38 | }, 39 | define: { 40 | timestamps: false 41 | }, 42 | //logging: console.log 43 | logging: false, 44 | }; 45 | 46 | return new Sequelize(config.db.dbName, config.db.username, config.db.password, options); 47 | } -------------------------------------------------------------------------------- /server/src/database/models/_modelHelper.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | 3 | const settings = { 4 | pluralTableNames: true 5 | }; 6 | 7 | export default { 8 | getName: getDbName, 9 | defineForeignKey, 10 | defineModel 11 | }; 12 | 13 | function getDbName(name) { 14 | return _.snakeCase(name); 15 | } 16 | 17 | function defineForeignKey(name) { 18 | return { 19 | name: name, 20 | field: getDbName(name) 21 | }; 22 | } 23 | 24 | function defineModel(name: string, fields, options, sequelize) { 25 | if (!options) options = {}; 26 | 27 | if (!options.tableName) { 28 | let tableName = name; 29 | 30 | if (settings.pluralTableNames) { 31 | //the same way it is done in sequelize 32 | tableName = sequelize.Utils.inflection.pluralize(name); 33 | } 34 | 35 | tableName = getDbName(tableName); 36 | 37 | options.tableName = tableName; 38 | } 39 | 40 | _.forEach(_.keys(fields), (fieldKey) => { 41 | fields[fieldKey].field = getDbName(fieldKey); 42 | }); 43 | 44 | return sequelize.define(name, fields, options); 45 | } -------------------------------------------------------------------------------- /server/src/database/models/course.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | 3 | export function init(sequelize, DataTypes) { 4 | let fields = { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | number: { 11 | type: DataTypes.INTEGER 12 | }, 13 | title: { 14 | type: DataTypes.STRING 15 | }, 16 | credits: { 17 | type: DataTypes.INTEGER 18 | } 19 | }; 20 | 21 | let options = { 22 | classMethods: { 23 | associate(models) { 24 | model.belongsToMany(models.Instructor, { 25 | through: helper.getName('courseInstructor'), 26 | foreignKey: helper.defineForeignKey('courseId') 27 | }); 28 | model.hasMany(models.Enrollment, { 29 | foreignKey: helper.defineForeignKey('courseId') 30 | }); 31 | model.belongsTo(models.Department, { 32 | foreignKey: helper.defineForeignKey('departmentId') 33 | }); 34 | } 35 | } 36 | }; 37 | 38 | let model = helper.defineModel('course', fields, options, sequelize); 39 | 40 | return model; 41 | } -------------------------------------------------------------------------------- /server/src/database/models/department.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | 3 | export function init(sequelize, DataTypes) { 4 | let fields = { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | name: { 11 | type: DataTypes.STRING 12 | }, 13 | budget: { 14 | type: DataTypes.DECIMAL 15 | }, 16 | startDate: { 17 | type: DataTypes.DATE 18 | } 19 | }; 20 | 21 | let options = { 22 | classMethods: { 23 | associate(models) { 24 | model.hasMany(models.Course, { 25 | foreignKey: helper.defineForeignKey('departmentId') 26 | }); 27 | model.belongsTo(models.Instructor, { 28 | foreignKey: helper.defineForeignKey('instructorId') 29 | }); 30 | } 31 | } 32 | }; 33 | 34 | let model = helper.defineModel('department', fields, options, sequelize); 35 | 36 | return model; 37 | } -------------------------------------------------------------------------------- /server/src/database/models/enrollment.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | 3 | export function init(sequelize, DataTypes) { 4 | let fields = { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | grade: { 11 | type: DataTypes.STRING 12 | } 13 | }; 14 | 15 | let options = { 16 | classMethods: { 17 | associate(models) { 18 | model.belongsTo(models.Student, { 19 | foreignKey: helper.defineForeignKey('studentId') 20 | }); 21 | model.belongsTo(models.Course, { 22 | foreignKey: helper.defineForeignKey('courseId') 23 | }); 24 | } 25 | } 26 | }; 27 | 28 | let model = helper.defineModel('enrollment', fields, options, sequelize); 29 | 30 | return model; 31 | } -------------------------------------------------------------------------------- /server/src/database/models/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as _ from 'lodash'; 4 | const Sequelize = require('sequelize'); 5 | 6 | const models = [ 7 | require('./course'), 8 | require('./department'), 9 | require('./enrollment'), 10 | require('./instructor'), 11 | require('./officeAssignment'), 12 | require('./student'), 13 | require('./user') 14 | ]; 15 | 16 | module.exports = { 17 | init: initModels 18 | }; 19 | 20 | function initModels(sequelize) { 21 | let result = {}; 22 | 23 | _.forEach(models, modelInit => { 24 | let model = modelInit.init(sequelize, Sequelize); 25 | result[_.upperFirst(model.name)] = model; 26 | }); 27 | 28 | _.forEach(_.keys(result), modelName => { 29 | if (result[modelName].associate) { 30 | result[modelName].associate(result); 31 | } 32 | }); 33 | 34 | return result; 35 | } 36 | -------------------------------------------------------------------------------- /server/src/database/models/instructor.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | 3 | export function init(sequelize, DataTypes) { 4 | let fields = { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | firstName: { 11 | type: DataTypes.STRING 12 | }, 13 | lastName: { 14 | type: DataTypes.STRING 15 | }, 16 | hireDate: { 17 | type: DataTypes.DATE 18 | } 19 | }; 20 | 21 | let options = { 22 | classMethods: { 23 | associate: function (models) { 24 | model.belongsToMany(models.Course, { 25 | through: helper.getName('courseInstructor'), 26 | foreignKey: helper.defineForeignKey('instructorId') 27 | }); 28 | model.hasOne(models.OfficeAssignment, { 29 | foreignKey: helper.defineForeignKey('instructorId') 30 | }); 31 | } 32 | }, 33 | instanceMethods: { 34 | getFullName() { 35 | return `${this.lastName}, ${this.firstName}`; 36 | } 37 | } 38 | }; 39 | 40 | let model = helper.defineModel('instructor', fields, options, sequelize); 41 | 42 | return model; 43 | } -------------------------------------------------------------------------------- /server/src/database/models/officeAssignment.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | 3 | export function init(sequelize, DataTypes) { 4 | let fields = { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | location: { 11 | type: DataTypes.STRING 12 | } 13 | }; 14 | 15 | let options = { 16 | classMethods: { 17 | associate(models) { 18 | model.belongsTo(models.Instructor, { 19 | foreignKey: helper.defineForeignKey('instructorId') 20 | }); 21 | } 22 | } 23 | }; 24 | 25 | let model = helper.defineModel('officeAssignment', fields, options, sequelize); 26 | 27 | return model; 28 | } -------------------------------------------------------------------------------- /server/src/database/models/student.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | 3 | export function init(sequelize, DataTypes) { 4 | let fields = { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true 9 | }, 10 | firstName: { 11 | type: DataTypes.STRING 12 | }, 13 | lastName: { 14 | type: DataTypes.STRING 15 | }, 16 | enrollmentDate: { 17 | type: DataTypes.DATE 18 | } 19 | }; 20 | 21 | let options = { 22 | classMethods: { 23 | associate(models) { 24 | model.hasMany(models.Enrollment, { 25 | foreignKey: helper.defineForeignKey('studentId') 26 | }); 27 | } 28 | }, 29 | instanceMethods: { 30 | getFullName() { 31 | return `${this.lastName}, ${this.firstName}`; 32 | } 33 | } 34 | }; 35 | 36 | let model = helper.defineModel('student', fields, options, sequelize); 37 | 38 | return model; 39 | } -------------------------------------------------------------------------------- /server/src/database/models/user.ts: -------------------------------------------------------------------------------- 1 | import helper from './_modelHelper'; 2 | const bcrypt = require('bcrypt-nodejs'); 3 | 4 | export function init(sequelize, DataTypes) { 5 | let fields = { 6 | id: { 7 | type: DataTypes.INTEGER, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | email: { 12 | type: DataTypes.STRING 13 | }, 14 | profile: { 15 | type: DataTypes.JSON //local, google, facebook 16 | } 17 | }; 18 | 19 | let options = { 20 | classMethods: { 21 | generateHash(password) { 22 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 23 | }, 24 | }, 25 | instanceMethods: { 26 | getFullName() { 27 | return `${this.lastName}, ${this.firstName}`; 28 | } 29 | } 30 | }; 31 | 32 | let model = helper.defineModel('user', fields, options, sequelize); 33 | 34 | return model; 35 | } -------------------------------------------------------------------------------- /server/src/database/seeders/seederDefault.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as Promise from 'bluebird'; 3 | import path from '../../helpers/pathHelper'; 4 | import * as moment from 'moment'; 5 | import config from '../../config'; 6 | 7 | export default { 8 | seedData 9 | }; 10 | 11 | async function seedData(db) { 12 | let seedPath = path.getDataRelative('seed/seedData.json'); 13 | let seedData = require(seedPath); 14 | 15 | await seedUsers(db, seedData.users); 16 | 17 | await seedInstructors(db, seedData.instructors); 18 | 19 | await seedOfficeAssignments(db, seedData.officeAssignments); 20 | 21 | await seedDepartments(db, seedData.departments); 22 | 23 | await seedCourses(db, seedData.courses); 24 | 25 | await seedStudents(db, seedData.students); 26 | 27 | await seedEnrollments(db, seedData.enrollments); 28 | 29 | await postImportRoutine(db); 30 | 31 | console.log('DB was seeded!'); 32 | } 33 | 34 | function seedUsers(db, usersData) { 35 | return Promise.resolve(usersData) 36 | .map((user) => { 37 | return db.models.User.create(user); 38 | }); 39 | } 40 | 41 | function seedDepartments(db, departmentsData) { 42 | return Promise.resolve(departmentsData) 43 | .map((department: any) => { 44 | department.startDate = parseDate(department.startDate); 45 | 46 | return db.models.Department.create(department); 47 | }); 48 | } 49 | 50 | function seedCourses(db, coursesData) { 51 | return Promise.resolve(coursesData) 52 | .map((course: any) => { 53 | return db.models.Course.create(course) 54 | .then((courseModel) => { 55 | courseModel.setInstructors(course.instructorsIds); 56 | }); 57 | }); 58 | } 59 | 60 | function seedStudents(db, studentsData) { 61 | return Promise.resolve(studentsData) 62 | .map((student) => { 63 | return db.models.Student.create(student); 64 | }); 65 | } 66 | 67 | function seedEnrollments(db, enrollmentsData) { 68 | return Promise.resolve(enrollmentsData) 69 | .map((enrollment: any) => { 70 | enrollment.enrollmentDate = parseDate(enrollment.enrollmentDate); 71 | 72 | return db.models.Enrollment.create(enrollment); 73 | }); 74 | } 75 | 76 | function seedInstructors(db, instructorsData) { 77 | return Promise.resolve(instructorsData) 78 | .map((instructor: any) => { 79 | instructor.hireDate = parseDate(instructor.hireDate); 80 | 81 | return db.models.Instructor.create(instructor); 82 | }); 83 | } 84 | 85 | function seedOfficeAssignments(db, officeAssignmentsData) { 86 | return Promise.resolve(officeAssignmentsData) 87 | .map((officeAssignment) => { 88 | return db.models.OfficeAssignment.create(officeAssignment); 89 | }); 90 | } 91 | 92 | function parseDate(dateStr) { 93 | return moment(dateStr, config.format.date).toDate(); 94 | } 95 | 96 | function postImportRoutine(db) { 97 | if (db.sequelize.dialect.name === 'postgres') { 98 | return Promise.resolve(_.toArray(db.models)) 99 | .map(model => { 100 | return updatePostgresSequence(model, db); 101 | }); 102 | } 103 | 104 | return Promise.resolve(null); 105 | } 106 | 107 | function updatePostgresSequence(model, db) { 108 | let tableName = model.tableName; 109 | let idField = model.autoIncrementField; 110 | let sql = `SELECT setval('${tableName}_id_seq', (SELECT MAX(${idField}) FROM ${tableName}));`; 111 | return db.sequelize.query(sql); 112 | } 113 | -------------------------------------------------------------------------------- /server/src/helpers/emailHelper.ts: -------------------------------------------------------------------------------- 1 | import * as Promise from 'bluebird'; 2 | import pathHelper from './pathHelper'; 3 | 4 | const nodemailer = require('nodemailer'); 5 | const EmailTemplate = require('email-templates').EmailTemplate; 6 | 7 | const emailTransport = nodemailer.createTransport(); 8 | 9 | export default { 10 | sendEmail, 11 | sendEmailTemplate 12 | }; 13 | 14 | interface EmailOptions { 15 | from: string, 16 | to: string, 17 | subject?: string, 18 | text?: string, 19 | html?: string 20 | } 21 | 22 | function sendEmail(emailOptions: EmailOptions): Promise { 23 | return new Promise((resolve, reject) => { 24 | emailTransport.sendMail(emailOptions, function (error, info) { 25 | if (error) return Promise.reject(error); 26 | 27 | return info; 28 | }); 29 | }); 30 | } 31 | 32 | function sendEmailTemplate(templateName: string, data: Object, emailData: EmailOptions) { 33 | return renderTemplate(templateName, data) 34 | .then((data) => { 35 | emailData.html = data.html; 36 | 37 | if (!emailData.subject) emailData.subject = data.subject; 38 | 39 | return new Promise((resolve, reject) => { 40 | emailTransport.sendMail(emailData, function (err, info) { 41 | if (err) return reject(err); 42 | 43 | return resolve(info); 44 | }); 45 | }); 46 | }); 47 | } 48 | 49 | function renderTemplate(name: string, data: Object): Promise { 50 | let templateDir = pathHelper.getDataRelative('emails', name); 51 | let template = new EmailTemplate(templateDir); 52 | 53 | return new Promise((resolve, reject) => { 54 | template.render(data, function (err, result) { 55 | if (err) reject(err); 56 | 57 | return resolve(result); 58 | }); 59 | }); 60 | } -------------------------------------------------------------------------------- /server/src/helpers/errorHelper.ts: -------------------------------------------------------------------------------- 1 | import logger from '../logger'; 2 | import textValue from './textValueHelper'; 3 | import config from '../config'; 4 | 5 | export default { 6 | logError, 7 | getErrorMessage 8 | }; 9 | 10 | function getErrorMessage(error): string { 11 | if (!error) return ''; 12 | 13 | if (error.isAppError) { 14 | if (!error.message) { 15 | let message = textValue.error(error.type, error.code, error.data); 16 | 17 | if (!message) message = `Cannot find error message for type:${error.type} code:${error.code}`; 18 | 19 | error.message = message; 20 | } 21 | 22 | if (error.uiShow) return error.message; 23 | } 24 | 25 | if (config.app.isDevLocal) { 26 | return error.message || error; 27 | } 28 | 29 | return 'Server Error'; 30 | } 31 | 32 | function logError(error) { 33 | if (!config.app.logErrors) return; 34 | 35 | if (error.isAppError && !error.log) return; 36 | 37 | logger.error(error); 38 | } -------------------------------------------------------------------------------- /server/src/helpers/pathHelper.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as _ from 'lodash'; 3 | 4 | let rootPath = path.join(__dirname, '../..'); 5 | let defaultDataPath = path.join(rootPath, 'data'); 6 | 7 | export default { 8 | path, 9 | getRelative: getRelativePath, 10 | getDataRelative: getDataRelativePath 11 | }; 12 | 13 | function getRelativePath(...paths: string[]) { 14 | let args = _.toArray(arguments); 15 | 16 | args.unshift(rootPath); 17 | 18 | return path.join.apply(this, args); 19 | } 20 | 21 | function getDataRelativePath(...paths: string[]) { 22 | let args = _.toArray(arguments); 23 | 24 | args.unshift(getDataPath()); 25 | 26 | return path.join.apply(this, args); 27 | } 28 | 29 | function getDataPath() { 30 | if (process.env['NODE_DATA_DIR']) { 31 | return process.env['NODE_DATA_DIR']; 32 | } 33 | 34 | return defaultDataPath; 35 | } -------------------------------------------------------------------------------- /server/src/helpers/textValueHelper.ts: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | import pathHelper from './/pathHelper'; 3 | 4 | const template = require('es6-template-strings'); 5 | const textValuesPath = pathHelper.getDataRelative('text', 'textValues.json'); 6 | const textValuesInfo = require(textValuesPath); 7 | 8 | export default { 9 | byKey, 10 | error, 11 | info, 12 | warning 13 | }; 14 | 15 | function byKey(key: string, data?: Object): string { 16 | let val = _.get(textValuesInfo, key); 17 | 18 | if (!val) return; 19 | 20 | let result = data ? template(val, data) : val; 21 | 22 | return result; 23 | } 24 | 25 | function error(type: string, code: string, data?: Object) { 26 | let key = `errors.${type}.${code}`; 27 | 28 | return byKey(key, data); 29 | } 30 | 31 | function info(type: string, code: string, data?: Object) { 32 | let key = `info.${type}.${code}`; 33 | 34 | return byKey(key, data); 35 | } 36 | 37 | function warning(type: string, code: string, data?: Object) { 38 | let key = `warning.${type}.${code}`; 39 | 40 | return byKey(key, data); 41 | } -------------------------------------------------------------------------------- /server/src/logger.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as moment from 'moment'; 3 | import * as winston from 'winston'; 4 | import pathHelper from './helpers/pathHelper'; 5 | import config from './config'; 6 | import AppError from './appError'; 7 | 8 | let errorLogger = null; 9 | let performanceLogger = null; 10 | let infoLogger = null; 11 | let performanceCache = {}; 12 | 13 | export default { 14 | error: logError, 15 | info: logInfo, 16 | timeStart: logTimeStart, 17 | timeEnd: logTimeEnd, 18 | logMessage: logMessage 19 | }; 20 | 21 | function initLoggers() { 22 | let getTransportFile = (logFileName) => 23 | new winston.transports.File({filename: pathHelper.getDataRelative('logs', logFileName)}); 24 | 25 | performanceLogger = new (winston.Logger)({ 26 | transports: [ 27 | getTransportFile('performance.log') 28 | ] 29 | }); 30 | 31 | errorLogger = new (winston.Logger)({ 32 | transports: [ 33 | getTransportFile('errors.log') 34 | ] 35 | }); 36 | 37 | if (config.app.logErrors) { 38 | winston.handleExceptions( 39 | (new (winston.transports.Console)()), 40 | getTransportFile('errors.log') 41 | ); 42 | } 43 | 44 | infoLogger = new (winston.Logger)({ 45 | transports: [ 46 | new (winston.transports.Console)(), 47 | getTransportFile('info.log') 48 | ] 49 | }); 50 | } 51 | 52 | initLoggers(); 53 | 54 | function logTimeStart(timerName) { 55 | if (!config.app.isDevLocal) return; 56 | 57 | if (performanceCache[timerName]) throw new AppError('Timer was already created. Timer name: ' + timerName); 58 | 59 | performanceCache[timerName] = new Date().getTime(); 60 | 61 | } 62 | 63 | function logTimeEnd(timerName) { 64 | if (!config.app.isDevLocal) return; 65 | 66 | if (!performanceCache[timerName]) throw new AppError('Timer was not previously created. Timer name: ' + timerName); 67 | 68 | let endTime = new Date().getTime(); 69 | let startTime = performanceCache[timerName]; 70 | 71 | let ms = endTime - startTime; 72 | performanceLogger.info('Timer ' + timerName + ': ' + moment.utc(ms).format('HH:mm:ss.SSS')); 73 | 74 | performanceCache = _.omit(performanceCache, timerName); 75 | } 76 | 77 | function logError(err) { 78 | if (_.isError(err)) { 79 | errorLogger.error('Error', {errorMessage: err.message, stack: err.stack}); 80 | return; 81 | } 82 | 83 | errorLogger.error(err); 84 | } 85 | 86 | function logInfo(message) { 87 | infoLogger.info(message); 88 | } 89 | 90 | function logMessage(message, metadata) { 91 | infoLogger.info(message, metadata); 92 | } -------------------------------------------------------------------------------- /server/src/repositories/_repositoryHelper.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import * as Promise from 'bluebird'; 3 | 4 | export default { 5 | stubData 6 | }; 7 | 8 | function stubData(data: any, delay = 500) { 9 | return Promise.delay(delay) 10 | .then(() => { 11 | return data; 12 | }); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /server/src/repositories/courseRepository.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | import * as Promise from 'bluebird'; 3 | import {Course} from '../../typings/app/models'; 4 | import AppError from '../appError'; 5 | 6 | export default { 7 | init, 8 | getCourses, 9 | getCourseById, 10 | updateCourse, 11 | addCourse, 12 | deleteCourse 13 | }; 14 | 15 | const db = dbInit.init(); 16 | let courseModel = db.models.Course; 17 | let departmentModel = db.models.Department; 18 | 19 | function init(db) { 20 | courseModel = db.models.Course; 21 | departmentModel = db.models.Department; 22 | } 23 | 24 | function getCourses(departmentId): Promise { 25 | let options = { 26 | include: departmentModel, 27 | where: {} 28 | }; 29 | 30 | if (departmentId) { 31 | options.where = {departmentId: departmentId}; 32 | } 33 | 34 | return courseModel.findAll(options); 35 | } 36 | 37 | function getCourseById(id): Promise { 38 | return courseModel.findById(id, { 39 | include: departmentModel 40 | }); 41 | } 42 | 43 | function updateCourse(courseData): Promise { 44 | return courseModel.findById(courseData.id) 45 | .then((course) => { 46 | if (!course) throw new AppError('app', 'course_not_found'); 47 | 48 | course.number = courseData.number; 49 | course.title = courseData.title; 50 | course.credits = courseData.credits; 51 | course.departmentId = courseData.departmentId; 52 | 53 | return course.save(); 54 | }); 55 | } 56 | 57 | function addCourse(course): Promise { 58 | return courseModel.create(course); 59 | } 60 | 61 | function deleteCourse(id): Promise { 62 | return courseModel.findById(id) 63 | .then((course) => { 64 | if (!course) throw new AppError('app', 'course_not_found'); 65 | 66 | return course.destroy(); 67 | }); 68 | } -------------------------------------------------------------------------------- /server/src/repositories/departmentRepository.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | import * as Promise from 'bluebird'; 3 | import {Department} from '../../typings/app/models'; 4 | import AppError from '../appError'; 5 | 6 | export default { 7 | init, 8 | getDepartments, 9 | getDepartmentById, 10 | updateDepartment, 11 | addDepartment, 12 | deleteDepartment 13 | }; 14 | 15 | const db = dbInit.init(); 16 | let departmentModel = db.models.Department; 17 | let instructorModel = db.models.Instructor; 18 | 19 | function init(db) { 20 | departmentModel = db.models.Department; 21 | instructorModel = db.models.Instructor; 22 | } 23 | 24 | function getDepartments(): Promise { 25 | return departmentModel.findAll({ 26 | include: instructorModel 27 | }); 28 | } 29 | 30 | function getDepartmentById(id): Promise { 31 | return departmentModel.findById(id, { 32 | include: instructorModel 33 | }); 34 | } 35 | 36 | function updateDepartment(departmentData): Promise { 37 | return departmentModel.findById(departmentData.id) 38 | .then((department) => { 39 | if (!department) throw new AppError('app', 'department_not_found'); 40 | 41 | department.name = departmentData.name; 42 | department.budget = departmentData.budget; 43 | department.startDate = departmentData.startDate; 44 | department.instructorId = departmentData.instructorId; 45 | 46 | return department.save(); 47 | }); 48 | } 49 | 50 | function addDepartment(department): Promise { 51 | return departmentModel.create(department); 52 | } 53 | 54 | function deleteDepartment(id): Promise { 55 | return departmentModel.findById(id) 56 | .then((department) => { 57 | if (!department) throw new AppError('app', 'department_not_found'); 58 | 59 | return department.destroy(); 60 | }); 61 | } -------------------------------------------------------------------------------- /server/src/repositories/enrollmentRepository.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | import * as Promise from 'bluebird'; 3 | import {Enrollment} from '../../typings/app/models'; 4 | 5 | export default { 6 | init, 7 | getEnrollmentsByCourseId 8 | }; 9 | 10 | const db = dbInit.init(); 11 | let enrollmentModel = db.models.Enrollment; 12 | let studentModel = db.models.Student; 13 | 14 | function init(db) { 15 | enrollmentModel = db.models.Enrollment; 16 | studentModel = db.models.Student; 17 | } 18 | 19 | 20 | function getEnrollmentsByCourseId(courseId): Promise { 21 | let options = { 22 | where: {courseId: courseId}, 23 | include: studentModel 24 | }; 25 | 26 | return enrollmentModel.findAll(options); 27 | } -------------------------------------------------------------------------------- /server/src/repositories/instructorRepository.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | import * as _ from 'lodash'; 3 | import * as Promise from 'bluebird'; 4 | import {Instructor} from '../../typings/app/models'; 5 | import AppError from '../appError'; 6 | 7 | export default { 8 | init, 9 | getInstructors, 10 | getInstructorById, 11 | updateInstructor, 12 | addInstructor, 13 | deleteInstructor 14 | }; 15 | 16 | const db = dbInit.init(); 17 | let instructorModel = db.models.Instructor; 18 | let courseModel = db.models.Course; 19 | let departmentModel = db.models.Department; 20 | let officeAssignmentModel = db.models.OfficeAssignment; 21 | 22 | function init(db) { 23 | instructorModel = db.models.Instructor; 24 | courseModel = db.models.Course; 25 | departmentModel = db.models.Department; 26 | officeAssignmentModel = db.models.OfficeAssignment; 27 | } 28 | 29 | function getInstructors(): Promise { 30 | let instructors: any = []; 31 | 32 | return instructorModel.findAll({ 33 | include: [ 34 | //include Course with Department 35 | { 36 | model: courseModel, include: [departmentModel] 37 | }, 38 | officeAssignmentModel 39 | ] 40 | }); 41 | } 42 | 43 | function getInstructorById(id): Promise { 44 | let options = { 45 | include: [ 46 | { 47 | model: courseModel, include: [departmentModel] 48 | }, 49 | officeAssignmentModel 50 | ] 51 | }; 52 | 53 | return instructorModel.findById(id, options); 54 | } 55 | 56 | function updateInstructor(instructorData): Promise { 57 | return instructorModel.findById(instructorData.id) 58 | .then((instructor) => { 59 | if (!instructor) throw new AppError('app', 'instructor_not_found'); 60 | 61 | instructor.firstName = instructorData.firstName; 62 | instructor.lastName = instructorData.lastName; 63 | instructor.hireDate = instructorData.hireDate; 64 | 65 | db.sequelize.transaction(tr => { 66 | let options = {transaction: tr}; 67 | let coursesIds = _.map(instructorData.courses, c => (c as any).id); 68 | 69 | return Promise.all([ 70 | instructor.save(options), 71 | instructor.setCourses(coursesIds, options) 72 | ]); 73 | }); 74 | 75 | return instructor.save(); 76 | }); 77 | } 78 | 79 | function addInstructor(instructorData): Promise { 80 | return instructorModel.create(instructorData) 81 | .then((instructor) => { 82 | let coursesIds = _.map(instructorData.courses, c => (c as any).id); 83 | 84 | instructor.setCourses(coursesIds); 85 | 86 | return instructor.save(); 87 | }); 88 | } 89 | 90 | function deleteInstructor(id): Promise { 91 | return instructorModel.findById(id) 92 | .then((instructor) => { 93 | if (!instructor) throw new AppError('app', 'instructor_not_found'); 94 | 95 | return instructor.destroy(); 96 | }); 97 | } -------------------------------------------------------------------------------- /server/src/repositories/officeAssignmentRepository.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | import * as Promise from 'bluebird'; 3 | import {OfficeAssignment} from '../../typings/app/models'; 4 | 5 | export default { 6 | init, 7 | saveOfficeAssignment, 8 | deleteOfficeAssignmentByInstructorId 9 | }; 10 | 11 | const db = dbInit.init(); 12 | let officeAssignmentModel = db.models.OfficeAssignment; 13 | 14 | function init(db) { 15 | officeAssignmentModel = db.models.OfficeAssignment; 16 | } 17 | 18 | function getOfficeAssignmentByInstructorId(instructorId) { 19 | let options = { 20 | where: {instructorId: instructorId} 21 | }; 22 | 23 | return officeAssignmentModel.findOne(options); 24 | } 25 | 26 | function saveOfficeAssignment(officeAssignment, instructorId): Promise { 27 | return getOfficeAssignmentByInstructorId(instructorId) 28 | .then((office) => { 29 | if (office) { 30 | office.location = officeAssignment.location; 31 | 32 | return office.save(); 33 | } 34 | 35 | if (!office && officeAssignment.location) { 36 | office = { 37 | location: officeAssignment.location, 38 | instructorId: instructorId 39 | }; 40 | 41 | return officeAssignmentModel.create(office); 42 | } 43 | 44 | return Promise.resolve(null); 45 | }); 46 | } 47 | 48 | function deleteOfficeAssignmentByInstructorId(instructorId): Promise { 49 | return getOfficeAssignmentByInstructorId(instructorId) 50 | .then((office) => { 51 | if (office) { 52 | return office.destroy(); 53 | } 54 | 55 | return Promise.resolve(null); 56 | }); 57 | } -------------------------------------------------------------------------------- /server/src/repositories/studentRepository.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | import * as Promise from 'bluebird'; 3 | import {Student} from '../../typings/app/models'; 4 | import AppError from '../appError'; 5 | 6 | export default { 7 | init, 8 | getStudentStatistics, 9 | getStudents, 10 | getStudentById, 11 | updateStudent, 12 | addStudent, 13 | deleteStudent 14 | }; 15 | 16 | const db = dbInit.init(); 17 | let studentModel = db.models.Student; 18 | let enrollmentModel = db.models.Enrollment; 19 | let courseModel = db.models.Course; 20 | 21 | function init(db) { 22 | studentModel = db.models.Student; 23 | enrollmentModel = db.models.Enrollment; 24 | courseModel = db.models.Course; 25 | } 26 | 27 | function getStudentStatistics() { 28 | let queryString = `SELECT enrollment_date as "enrollmentDate", COUNT(*) AS "studentCount" 29 | FROM students GROUP BY enrollment_date`; 30 | 31 | return db.sequelize.query(queryString) 32 | .then((data) => { 33 | return data[0]; 34 | }); 35 | } 36 | 37 | function getStudents(search, sortOrder, pageNumber, pageSize) { 38 | let orderParams = getSortOrder(sortOrder); 39 | 40 | let options = { 41 | where: { 42 | $or: [ 43 | {firstName: {$like: `%${search}%`}}, 44 | {lastName: {$like: `%${search}%`}} 45 | ] 46 | }, 47 | offset: (pageNumber - 1) * pageSize, 48 | limit: pageSize, 49 | order: [[orderParams.order, orderParams.direction]] 50 | }; 51 | 52 | return studentModel.findAndCountAll(options); 53 | } 54 | 55 | function getStudentById(id): Promise { 56 | return studentModel.findById(id, { 57 | include: [ 58 | {model: enrollmentModel, include: [courseModel]} 59 | ]} 60 | ); 61 | } 62 | 63 | function updateStudent(stud): Promise { 64 | return studentModel.findById(stud.id) 65 | .then((student) => { 66 | if (!student) throw new AppError('app', 'student_not_found'); 67 | 68 | student.firstName = stud.firstName; 69 | student.lastName = stud.lastName; 70 | student.enrollmentDate = stud.enrollmentDate; 71 | 72 | return student.save(); 73 | }); 74 | } 75 | 76 | function addStudent(student): Promise { 77 | return studentModel.create(student); 78 | } 79 | 80 | function deleteStudent(id): Promise { 81 | return studentModel.findById(id) 82 | .then((student) => { 83 | if (!student) throw new AppError('app', 'student_not_found'); 84 | 85 | return student.destroy(); 86 | }); 87 | } 88 | 89 | function getSortOrder(sortOrder) { 90 | let result: any = {}; 91 | 92 | switch (sortOrder) { 93 | case 'name': 94 | result = {order: 'lastName', direction: 'ASC'}; 95 | break; 96 | case 'date': 97 | result = {order: 'enrollmentDate', direction: 'ASC'}; 98 | break; 99 | case 'date_desc': 100 | result = {order: 'enrollmentDate', direction: 'DESC'}; 101 | break; 102 | default: 103 | result = {order: 'lastName', direction: 'DESC'}; 104 | break; 105 | } 106 | 107 | return result; 108 | } -------------------------------------------------------------------------------- /server/src/routes/routesHelper.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash'; 2 | import config from '../config'; 3 | let app = null; 4 | let passport = null; 5 | 6 | export default init; 7 | 8 | function init(expressApp, passportAuth) { 9 | app = expressApp; 10 | passport = passportAuth; 11 | return { 12 | app, 13 | isLoggedIn, 14 | get: httpGet, 15 | post: httpPost, 16 | put: httpPut, 17 | delete: httpDelete 18 | }; 19 | } 20 | 21 | interface AccessMode { 22 | auth?: boolean, 23 | view?: boolean 24 | } 25 | 26 | function httpGet(path, handler, accessMode?: AccessMode) { 27 | const args = getRouteArguments(path, handler, accessMode); 28 | app.get.apply(app, args); 29 | } 30 | 31 | function httpPost(path, handler, accessMode?: AccessMode) { 32 | const args = getRouteArguments(path, handler, accessMode); 33 | app.post.apply(app, args); 34 | } 35 | 36 | function httpPut(path, handler, accessMode?: AccessMode) { 37 | const args = getRouteArguments(path, handler, accessMode); 38 | app.put.apply(app, args); 39 | } 40 | 41 | function httpDelete(path, handler, accessMode?: AccessMode) { 42 | const args = getRouteArguments(path, handler, accessMode); 43 | app.delete.apply(app, args); 44 | } 45 | 46 | function getRouteArguments(path, handler, accessMode) { 47 | let result = []; 48 | 49 | accessMode = normalizeAccessMode(accessMode); 50 | 51 | result.push(path); 52 | let accessHandlers = getAccessCheck(accessMode); 53 | result = result.concat(accessHandlers); 54 | result.push(handler); 55 | 56 | return result; 57 | } 58 | 59 | function normalizeAccessMode(accessMode?: AccessMode): AccessMode { 60 | if (!accessMode) { 61 | return { 62 | auth: true, 63 | view: false 64 | }; 65 | } 66 | 67 | if (_.isUndefined(accessMode.auth)) { 68 | accessMode.auth = true; 69 | } 70 | 71 | return accessMode; 72 | } 73 | 74 | function getAccessCheck(accessMode: AccessMode) { 75 | if (!accessMode.auth) return []; 76 | 77 | if (accessMode.view) { 78 | return [isLoggedInView]; 79 | } 80 | else { 81 | return [isLoggedIn]; 82 | } 83 | } 84 | 85 | function isLoggedIn(req, res, next) { 86 | if (!config.auth.useAuth) return next(); 87 | 88 | if (req.isAuthenticated()) return next(); 89 | 90 | res.send(401, 'Unauthorized'); 91 | } 92 | 93 | function isLoggedInView(req, res, next) { 94 | if (!config.auth.useAuth) return next(); 95 | 96 | if (req.isAuthenticated()) return next(); 97 | 98 | res.redirect('/login'); 99 | } -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | import * as morgan from 'morgan'; 3 | import * as bodyParser from 'body-parser'; 4 | import * as compression from 'compression'; 5 | import * as cors from 'cors'; 6 | import * as _ from 'lodash'; 7 | 8 | import config from './config'; 9 | import pathHelper from './helpers/pathHelper'; 10 | import routes from './routes/routes'; 11 | import logger from './logger'; 12 | import auth from './auth/authInit'; 13 | 14 | const app = express(); 15 | 16 | export default { 17 | start 18 | }; 19 | 20 | function start(options: any) { 21 | initExpress(); 22 | 23 | initViewEngine(); 24 | 25 | const passport = require('passport'); 26 | 27 | routes.init(app, passport); 28 | 29 | //should be after routes.init 30 | initErrorHandling(app); 31 | 32 | app.listen(config.web.port, function () { 33 | console.log(`Server is listening on port ${config.web.port}!`); 34 | }); 35 | } 36 | 37 | function initExpress() { 38 | if (config.app.isDevLocal) app.use(morgan('dev')); //log requests 39 | 40 | app.use(bodyParser.json()); // get information from html forms 41 | app.use(bodyParser.urlencoded({extended: true})); 42 | 43 | app.use('/static', express.static(pathHelper.getRelative('../client/build/static'))); 44 | 45 | app.use(compression()); 46 | 47 | if (config.app.isDevLocal) app.use(cors()); 48 | 49 | //NOTE following required for auth only 50 | 51 | initSession(); 52 | 53 | initAuth(); 54 | } 55 | 56 | function initViewEngine() { 57 | const hbs = require('express-hbs'); 58 | const viewsDir = pathHelper.getRelative('views'); 59 | const entities = require('entities'); 60 | 61 | // Hook in express-hbs and tell it where known directories reside 62 | app.engine('hbs', hbs.express4({ 63 | partialsDir: pathHelper.getRelative('views/partials'), 64 | layoutsDir: pathHelper.getRelative('views/layouts'), 65 | defaultLayout: pathHelper.getRelative('views/layouts/auth.hbs') 66 | })); 67 | 68 | hbs.registerHelper('json', function(obj) { 69 | let jsonValue = JSON.stringify(obj); 70 | 71 | let val = entities.encodeHTML(jsonValue); 72 | 73 | return new hbs.SafeString(val); 74 | }); 75 | 76 | app.set('view engine', 'hbs'); 77 | app.set('views', viewsDir); 78 | } 79 | 80 | function initAuth() { 81 | const flash = require('connect-flash'); 82 | app.use(flash()); 83 | 84 | const passport = require('passport'); 85 | 86 | auth(passport); 87 | 88 | app.use(passport.initialize()); 89 | app.use(passport.session()); // persistent login sessions 90 | 91 | return passport; 92 | } 93 | 94 | function initSession() { 95 | const cookieParser = require('cookie-parser'); 96 | app.use(cookieParser()); 97 | 98 | const session = require('cookie-session'); 99 | app.use(session({ 100 | secret: config.web.sessionSecret 101 | })); 102 | } 103 | 104 | function initErrorHandling(app: express.Application) { 105 | //log unhandled errors 106 | (app as any).use(function (err, req, res, next) { 107 | logger.error(err); 108 | 109 | console.log(err); 110 | 111 | let message = _.isError(err) ? err.message : err; 112 | message = config.app.isDevLocal ? message : 'Server Error'; 113 | 114 | res.status(500).send({error: message}); 115 | }); 116 | 117 | process.on('uncaughtException', function (err) { 118 | logger.error(err); 119 | }); 120 | } -------------------------------------------------------------------------------- /server/src/startServer.ts: -------------------------------------------------------------------------------- 1 | process.on('uncaughtException', function (err) { 2 | let stack = err.stack; 3 | console.log(`Uncaught exception. ${err}`); 4 | }); 5 | 6 | import server from './server'; 7 | 8 | server.start({}); -------------------------------------------------------------------------------- /server/src/tasks/syncDb.ts: -------------------------------------------------------------------------------- 1 | import dbInit from '../database/database'; 2 | const db = dbInit.init(); 3 | 4 | import seeder from '../database/seeders/seederDefault'; 5 | 6 | db.sequelize.sync({force: true}) 7 | .then(() => { 8 | return seeder.seedData(db); 9 | }); -------------------------------------------------------------------------------- /server/src/tasks/typingsSymLink.ts: -------------------------------------------------------------------------------- 1 | import pathHelper from '../helpers/pathHelper'; 2 | import * as fs from 'fs'; 3 | 4 | let originalPath = pathHelper.getRelative('typings'); 5 | let destinationPath = pathHelper.getRelative('client', 'typings'); 6 | 7 | fs.symlink(originalPath, destinationPath, 'dir', (err) => { 8 | if (err) return console.log(err); 9 | 10 | console.log('Symlink successfully created!'); 11 | }); -------------------------------------------------------------------------------- /server/src/tests/_testHelper.ts: -------------------------------------------------------------------------------- 1 | import seeder from '../database/seeders/seederDefault'; 2 | import * as Promise from 'bluebird'; 3 | import pathHelper from '../helpers/pathHelper'; 4 | 5 | const dbPath = pathHelper.getDataRelative('db', 'test.db'); 6 | 7 | import dbInit from '../database/database'; 8 | const db = dbInit.init(dbPath); 9 | 10 | let dbIsInitialized = false; 11 | 12 | export default { 13 | initTestDb 14 | }; 15 | 16 | function initTestDb() { 17 | if (dbIsInitialized) return Promise.resolve(db); 18 | 19 | return db.sequelize.sync({force: true}) 20 | .then(() => { 21 | return seeder.seedData(db); 22 | }) 23 | .then(() => { 24 | dbIsInitialized = true; 25 | return db; 26 | }) 27 | .catch((err) => { 28 | console.log(err); 29 | }); 30 | } -------------------------------------------------------------------------------- /server/src/tests/departmentRepositoryTest.ts: -------------------------------------------------------------------------------- 1 | import helper from './_testHelper'; 2 | import departmentRepository from '../repositories/departmentRepository'; 3 | import {expect} from 'chai'; 4 | 5 | describe('Department Repository', () => { 6 | before((done) => { 7 | helper.initTestDb() 8 | .then((db) => { 9 | departmentRepository.init(db); 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('Departments', () => { 15 | it('get all', (done) => { 16 | return departmentRepository.getDepartments() 17 | .then((departments) => { 18 | expect(departments).not.to.be.null; 19 | expect(departments).to.have.length(4); 20 | expect(departments[0].name).to.be.equal('English'); 21 | expect(departments[0].instructor).not.to.be.null; 22 | expect(departments[0].instructor.firstName).to.be.equal('Kim'); 23 | done(); 24 | }) 25 | .catch(function(err) { 26 | done(err); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('Department', () => { 32 | it('create', (done) => { 33 | let department = { 34 | name: 'Test Department', 35 | budget: 100000, 36 | startDate: '09/01/2007', 37 | instructorId: 2 38 | }; 39 | 40 | return departmentRepository.addDepartment(department) 41 | .then((data) => { 42 | expect(data).not.to.be.null; 43 | expect(data.name).to.be.equal(department.name); 44 | expect(data.budget).to.be.equal(department.budget.toString()); 45 | done(); 46 | }) 47 | .catch(function(err) { 48 | done(err); 49 | }); 50 | }); 51 | 52 | it('update', (done) => { 53 | let department = { 54 | id: 5, 55 | name: 'Test Department updated', 56 | budget: 100001.00, 57 | startDate: '10/01/2007', 58 | instructorId: 3 59 | }; 60 | 61 | return departmentRepository.updateDepartment(department) 62 | .then((data) => { 63 | expect(data).not.to.be.null; 64 | expect(data.name).to.be.equal(department.name); 65 | expect(data.budget).to.be.equal(department.budget); 66 | done(); 67 | }) 68 | .catch(function(err) { 69 | done(err); 70 | }); 71 | }); 72 | 73 | it('get by id', (done) => { 74 | return departmentRepository.getDepartmentById(5) 75 | .then((department) => { 76 | expect(department).not.to.be.null; 77 | expect(department.name).to.be.equal('Test Department updated'); 78 | expect(department.instructor).not.to.be.null; 79 | expect(department.instructor.firstName).to.be.equal('Roger'); 80 | expect(department.instructor.lastName).to.be.equal('Harui'); 81 | done(); 82 | }) 83 | .catch(function(err) { 84 | done(err); 85 | }); 86 | }); 87 | 88 | it('delete', (done) => { 89 | return departmentRepository.deleteDepartment(5) 90 | .then((data) => { 91 | expect(data).not.to.be.null; 92 | done(); 93 | }) 94 | .catch(function(err) { 95 | done(err); 96 | }); 97 | }); 98 | }); 99 | }); -------------------------------------------------------------------------------- /server/src/tests/enrollmentRepositoryTest.ts: -------------------------------------------------------------------------------- 1 | import helper from './_testHelper'; 2 | import enrollmentRepository from '../repositories/enrollmentRepository'; 3 | import {expect} from 'chai'; 4 | 5 | describe('Enrollment Repository', () => { 6 | before((done) => { 7 | helper.initTestDb() 8 | .then((db) => { 9 | enrollmentRepository.init(db); 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('Enrollments', () => { 15 | it('get by course id', (done) => { 16 | let courseId = 2; 17 | 18 | return enrollmentRepository.getEnrollmentsByCourseId(courseId) 19 | .then((enrollments) => { 20 | expect(enrollments).not.to.be.null; 21 | expect(enrollments).to.have.length(3); 22 | expect(enrollments[0].student).not.to.be.null; 23 | expect(enrollments[0].student).to.have.property('firstName'); 24 | done(); 25 | }) 26 | .catch(function(err) { 27 | done(err); 28 | }); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /server/src/tests/instructorRepositoryTest.ts: -------------------------------------------------------------------------------- 1 | import helper from './_testHelper'; 2 | import instructorRepository from '../repositories/instructorRepository'; 3 | import {expect} from 'chai'; 4 | 5 | describe('Instructor Repository', () => { 6 | before((done) => { 7 | helper.initTestDb() 8 | .then((db) => { 9 | instructorRepository.init(db); 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('Instructors', () => { 15 | it('get all', (done) => { 16 | return instructorRepository.getInstructors() 17 | .then((instructors) => { 18 | expect(instructors).not.to.be.null; 19 | expect(instructors).to.have.length(5); 20 | expect(instructors[0]).to.have.property('firstName'); 21 | expect(instructors[0].courses.length).to.be.greaterThan(0); 22 | expect(instructors[0].courses[0].department).not.to.be.null; 23 | done(); 24 | }) 25 | .catch(function(err) { 26 | done(err); 27 | }); 28 | }); 29 | }); 30 | 31 | describe('Instructor', () => { 32 | it('create', (done) => { 33 | let instructor = { 34 | firstName: 'Test name', 35 | lastName: 'Test last name', 36 | hireDate: '11/03/1995', 37 | courses: [ 38 | {id: 1}, 39 | {id: 2} 40 | ] 41 | }; 42 | 43 | return instructorRepository.addInstructor(instructor) 44 | .then((data) => { 45 | expect(data).not.to.be.null; 46 | expect(data.firstName).to.be.equal(instructor.firstName); 47 | expect(data.lastName).to.be.equal(instructor.lastName); 48 | done(); 49 | }) 50 | .catch(function(err) { 51 | done(err); 52 | }); 53 | }); 54 | 55 | it('update', (done) => { 56 | let instructor = { 57 | id: 6, 58 | firstName: 'Name updated', 59 | lastName: 'Last name updated', 60 | hireDate: '11/03/2015', 61 | courses: [ 62 | {id: 2}, 63 | {id: 3} 64 | ] 65 | }; 66 | 67 | return instructorRepository.updateInstructor(instructor) 68 | .then((data) => { 69 | expect(data).not.to.be.null; 70 | expect(data.firstName).to.be.equal(instructor.firstName); 71 | expect(data.lastName).to.be.equal(instructor.lastName); 72 | done(); 73 | }) 74 | .catch(function(err) { 75 | done(err); 76 | }); 77 | }); 78 | 79 | it('get by id', (done) => { 80 | return instructorRepository.getInstructorById(6) 81 | .then((instructor) => { 82 | expect(instructor).not.to.be.null; 83 | expect(instructor.firstName).to.be.equal('Name updated'); 84 | expect(instructor.courses).to.have.length(2); 85 | expect(instructor.courses[0].department).not.to.be.null; 86 | done(); 87 | }) 88 | .catch(function(err) { 89 | done(err); 90 | }); 91 | }); 92 | 93 | it('delete', (done) => { 94 | return instructorRepository.deleteInstructor(6) 95 | .then((data) => { 96 | expect(data).not.to.be.null; 97 | done(); 98 | }) 99 | .catch(function(err) { 100 | done(err); 101 | }); 102 | }); 103 | }); 104 | }); -------------------------------------------------------------------------------- /server/src/tests/officeAssignmentRepositoryTest.ts: -------------------------------------------------------------------------------- 1 | import helper from './_testHelper'; 2 | import officeAssignmentRepository from '../repositories/officeAssignmentRepository'; 3 | import {expect} from 'chai'; 4 | 5 | describe('Office Assignment Repository', () => { 6 | before((done) => { 7 | helper.initTestDb() 8 | .then((db) => { 9 | officeAssignmentRepository.init(db); 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('Office Assignment', () => { 15 | it('create', (done) => { 16 | let officeAssignment = { 17 | location: 'Test Location' 18 | }; 19 | let instructorId = 5; 20 | 21 | return officeAssignmentRepository.saveOfficeAssignment(officeAssignment, instructorId) 22 | .then((data) => { 23 | expect(data).not.to.be.null; 24 | expect(data.location).to.be.equal(officeAssignment.location); 25 | done(); 26 | }) 27 | .catch(function(err) { 28 | done(err); 29 | }); 30 | }); 31 | 32 | it('update', (done) => { 33 | let officeAssignment = { 34 | location: 'Test Location Updated' 35 | }; 36 | let instructorId = 5; 37 | 38 | return officeAssignmentRepository.saveOfficeAssignment(officeAssignment, instructorId) 39 | .then((data) => { 40 | expect(data).not.to.be.null; 41 | expect(data.location).to.be.equal(officeAssignment.location); 42 | done(); 43 | }) 44 | .catch(function(err) { 45 | done(err); 46 | }); 47 | }); 48 | 49 | it('delete', (done) => { 50 | let instructorId = 5; 51 | 52 | return officeAssignmentRepository.deleteOfficeAssignmentByInstructorId(instructorId) 53 | .then((data) => { 54 | expect(data).not.to.be.null; 55 | done(); 56 | }) 57 | .catch(function(err) { 58 | done(err); 59 | }); 60 | }); 61 | }); 62 | }); -------------------------------------------------------------------------------- /server/src/tests/studentRepositoryTest.ts: -------------------------------------------------------------------------------- 1 | import helper from './_testHelper'; 2 | import studentRepository from '../repositories/studentRepository'; 3 | import {expect} from 'chai'; 4 | 5 | describe('Student Repository', () => { 6 | before((done) => { 7 | helper.initTestDb() 8 | .then((db) => { 9 | studentRepository.init(db); 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('Students', () => { 15 | it('get all', (done) => { 16 | let search = ''; 17 | let sortOrder = ''; 18 | let pageNumber = 1; 19 | let pageSize = 9; 20 | 21 | return studentRepository.getStudents(search, sortOrder, pageNumber, pageSize) 22 | .then((data) => { 23 | expect(data).not.to.be.null; 24 | expect(data.count).to.be.equal(8); 25 | expect(data.rows).to.have.length(8); 26 | expect(data.rows[0]).to.have.property('firstName'); 27 | done(); 28 | }) 29 | .catch(function(err) { 30 | done(err); 31 | }); 32 | }); 33 | 34 | it('get statistics', (done) => { 35 | return studentRepository.getStudentStatistics() 36 | .then((data) => { 37 | expect(data).not.to.be.null; 38 | expect(data).to.have.length(5); 39 | done(); 40 | }) 41 | .catch(function(err) { 42 | done(err); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('Student', () => { 48 | it('create', (done) => { 49 | let student = { 50 | firstName: 'Test name', 51 | lastName: 'Test last name', 52 | enrollmentDate: '11/03/1995' 53 | }; 54 | 55 | return studentRepository.addStudent(student) 56 | .then((data) => { 57 | expect(data).not.to.be.null; 58 | expect(data.firstName).to.be.equal(student.firstName); 59 | expect(data.lastName).to.be.equal(student.lastName); 60 | done(); 61 | }) 62 | .catch(function(err) { 63 | done(err); 64 | }); 65 | }); 66 | 67 | it('update', (done) => { 68 | let student = { 69 | id: 9, 70 | firstName: 'Name updated', 71 | lastName: 'Last name updated', 72 | enrollmentDate: '11/03/2015' 73 | }; 74 | 75 | return studentRepository.updateStudent(student) 76 | .then((data) => { 77 | expect(data).not.to.be.null; 78 | expect(data.firstName).to.be.equal(data.firstName); 79 | expect(data.lastName).to.be.equal(data.lastName); 80 | done(); 81 | }) 82 | .catch(function(err) { 83 | done(err); 84 | }); 85 | }); 86 | 87 | it('get by id', (done) => { 88 | return studentRepository.getStudentById(9) 89 | .then((student) => { 90 | expect(student).not.to.be.null; 91 | expect(student.firstName).to.be.equal('Name updated'); 92 | done(); 93 | }) 94 | .catch(function(err) { 95 | done(err); 96 | }); 97 | }); 98 | 99 | it('delete', (done) => { 100 | return studentRepository.deleteStudent(9) 101 | .then((data) => { 102 | expect(data).not.to.be.null; 103 | done(); 104 | }) 105 | .catch(function(err) { 106 | done(err); 107 | }); 108 | }); 109 | }); 110 | }); -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "outDir": "./build", 7 | "rootDir": "src", 8 | "allowUnreachableCode": true 9 | }, 10 | "exclude": [ 11 | ".", 12 | "node_modules" 13 | ] 14 | } -------------------------------------------------------------------------------- /server/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "indent": [ 5 | true, 6 | "spaces" 7 | ], 8 | "no-duplicate-variable": true, 9 | "no-eval": true, 10 | "no-internal-module": true, 11 | "no-trailing-whitespace": false, 12 | "no-var-keyword": true, 13 | "one-line": [ 14 | true, 15 | "check-open-brace", 16 | "check-whitespace" 17 | ], 18 | "quotemark": [ 19 | true, 20 | "single", 21 | "jsx-double" 22 | ], 23 | "semicolon": [ 24 | true, 25 | "always", 26 | "ignore-interfaces" 27 | ], 28 | "triple-equals": [ 29 | true, 30 | "allow-null-check" 31 | ], 32 | "typedef-whitespace": [ 33 | true, 34 | { 35 | "call-signature": "nospace", 36 | "index-signature": "nospace", 37 | "parameter": "nospace", 38 | "property-declaration": "nospace", 39 | "variable-declaration": "nospace" 40 | } 41 | ], 42 | "variable-name": [ 43 | true, 44 | "ban-keywords" 45 | ], 46 | "whitespace": [ 47 | true, 48 | "check-branch", 49 | "check-decl", 50 | "check-operator", 51 | "check-separator", 52 | "check-type" 53 | ] 54 | } 55 | } -------------------------------------------------------------------------------- /server/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chai": "registry:npm/chai#3.5.0+20160415060238", 4 | "lodash": "registry:npm/lodash#4.0.0+20160416211519", 5 | "rimraf": "registry:npm/rimraf#2.5.0+20160211003958" 6 | }, 7 | "globalDependencies": { 8 | "bluebird": "registry:dt/bluebird#2.0.0+20160319051630", 9 | "body-parser": "registry:dt/body-parser#0.0.0+20160317120654", 10 | "compression": "registry:dt/compression#0.0.0+20160501162003", 11 | "config": "registry:dt/config#0.0.0+20160317120654", 12 | "cors": "registry:dt/cors#0.0.0+20160708202709", 13 | "express": "registry:dt/express#4.0.0+20160317120654", 14 | "express-serve-static-core": "registry:dt/express-serve-static-core#0.0.0+20160602151406", 15 | "joi": "registry:dt/joi#9.0.0+20160626220651", 16 | "mime": "registry:dt/mime#0.0.0+20160316155526", 17 | "mocha": "registry:dt/mocha#2.2.5+20160317120654", 18 | "moment": "registry:dt/moment#2.8.0+20160316155526", 19 | "moment-node": "registry:dt/moment-node#2.11.1+20160511043338", 20 | "morgan": "registry:dt/morgan#1.7.0+20160524142355", 21 | "node": "registry:dt/node#6.0.0+20160602155235", 22 | "passport": "registry:dt/passport#0.2.0+20160317120654", 23 | "passport-facebook": "registry:dt/passport-facebook#1.0.3+20160316155526", 24 | "passport-google-oauth": "registry:dt/passport-google-oauth#1.0.3+20160316155526", 25 | "passport-local": "registry:dt/passport-local#0.0.0+20160316155526", 26 | "serve-static": "registry:dt/serve-static#0.0.0+20160501131543", 27 | "shortid": "registry:dt/shortid#0.0.0+20160316155526", 28 | "winston": "registry:dt/winston#0.0.0+20160417152829" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/typings/app/custom.d.ts: -------------------------------------------------------------------------------- 1 | interface ErrorConstructor { 2 | captureStackTrace(thisArg: any, func: any): void 3 | } 4 | 5 | interface Window { 6 | devToolsExtension: Function 7 | } -------------------------------------------------------------------------------- /server/typings/app/models.d.ts: -------------------------------------------------------------------------------- 1 | //TODO use some Model interfaces 2 | export interface DbModels { 3 | User: any, 4 | Course: any, 5 | Department: any, 6 | Enrollment: any, 7 | Instructor: any, 8 | OfficeAssignment: any, 9 | Student: any 10 | } 11 | 12 | export interface User { 13 | id: number, 14 | email: string, 15 | profile: { 16 | local?: LocalProfile, 17 | google?: GoogleProfile 18 | }, 19 | getFullName(): string, 20 | save(): any 21 | } 22 | 23 | export interface LocalProfile { 24 | password: boolean, 25 | isActivated: boolean, 26 | activation: { 27 | created: string, 28 | token: string 29 | } 30 | reset: { 31 | created: string, 32 | token: string 33 | } 34 | } 35 | 36 | export interface GoogleProfile { 37 | id: number, 38 | token: string, 39 | name: string, 40 | email: string 41 | } 42 | 43 | export interface Course { 44 | id: number, 45 | number: number, 46 | title: string, 47 | credits: number, 48 | department?: Department 49 | } 50 | 51 | export interface Department { 52 | id: number, 53 | name: string, 54 | budget: number, 55 | startDate: Date, 56 | instructor?: Instructor 57 | } 58 | 59 | export interface Enrollment { 60 | id: number, 61 | grade: string, 62 | student?: Student 63 | } 64 | 65 | export interface Instructor { 66 | id: number, 67 | firstName: string, 68 | lastName: string, 69 | fullName: string, 70 | hireDate: Date, 71 | courses?: Course[] 72 | getFullName(): string 73 | } 74 | 75 | export interface OfficeAssignment { 76 | id: number, 77 | location: string, 78 | instructorId: number 79 | } 80 | 81 | export interface Student { 82 | id: number, 83 | firstName: string, 84 | lastName: string, 85 | fullName: string, 86 | enrollmentDate: Date, 87 | getFullName(): string 88 | } -------------------------------------------------------------------------------- /server/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | -------------------------------------------------------------------------------- /server/views/layouts/auth.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{appName}} 6 | 7 | 8 | 9 | 10 | {{{body}}} 11 | 12 | -------------------------------------------------------------------------------- /server/views/login.hbs: -------------------------------------------------------------------------------- 1 | {{!< auth}} 2 |
3 |
4 | 5 |
6 | {{#if title}} 7 |

{{title}}

8 | {{/if}} 9 |
10 |
11 | 12 |

Login

13 | 14 | {{#if statusMessage}} 15 |
{{statusMessage.message}}
16 | {{/if}} 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 | 33 | Forgot your password? 34 | 35 |
36 | 37 | Facebook 38 | Google+ 39 | 40 |
41 | 42 |

Need an account? Signup

43 |

Or go home.

44 | 45 |
46 |
47 |
48 | 49 |
50 |
-------------------------------------------------------------------------------- /server/views/partials/loginRedirectTip.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Already have an account? Login

3 |

Or go home.

-------------------------------------------------------------------------------- /server/views/password-forgot.hbs: -------------------------------------------------------------------------------- 1 | {{!< auth}} 2 |
3 |
4 | 5 |
6 | {{#if title}} 7 |

{{title}}

8 | {{/if}} 9 |
10 |
11 |

Reset Password

12 | 13 | {{#if statusMessage}} 14 |
{{statusMessage.message}}
15 | {{/if}} 16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 | 24 |
25 | 26 | {{> loginRedirectTip}} 27 | 28 |
29 |
30 |
31 | 32 |
33 |
-------------------------------------------------------------------------------- /server/views/password-reset.hbs: -------------------------------------------------------------------------------- 1 | {{!< auth}} 2 |
3 |
4 | 5 |
6 | {{#if title}} 7 |

{{title}}

8 | {{/if}} 9 |
10 |
11 |

Reset Password

12 | 13 | {{#if statusMessage}} 14 |
{{statusMessage.message}}
15 | {{/if}} 16 | 17 | {{#if message}} 18 |

{{message}}

19 | {{/if}} 20 | 21 |
22 |
{{passwordMessage}}
23 | 24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 |
38 | 39 | {{> loginRedirectTip}} 40 | 41 |
42 |
43 |
44 | 45 |
46 |
-------------------------------------------------------------------------------- /server/views/signup.hbs: -------------------------------------------------------------------------------- 1 | {{!< auth}} 2 |
3 |
4 | 5 |
6 | {{#if title}} 7 |

{{title}}

8 | {{/if}} 9 |
10 |
11 |

Signup

12 | 13 | {{#if statusMessage}} 14 |
{{statusMessage.message}}
15 | {{/if}} 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | {{> loginRedirectTip}} 35 | 36 |
37 |
38 |
39 | 40 |
41 |
--------------------------------------------------------------------------------