├── .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 |
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 |
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 | Number |
11 | Title |
12 | Credits |
13 | Department |
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 |
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 |
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 | Name |
11 | Budget |
12 | Start Date |
13 | Administrator |
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 |
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 | Number |
33 | Title |
34 | Department |
35 |
36 |
37 |
38 | {courses.map(course =>
39 | this.props.onSelectClick(course.id)}
42 | />
43 | )}
44 |
45 |
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 |
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 | Last Name |
10 | First Name |
11 | Hire Date |
12 | Office |
13 | Courses |
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 |
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 | Name |
15 | Grade |
16 |
17 |
18 |
19 | {enrollments.map(enrollment =>
20 |
21 | )}
22 |
23 |
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 | Course Title |
10 | Grade |
11 |
12 |
13 |
14 | {enrollments.map(enrollment =>
15 |
16 | )}
17 |
18 |
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 |
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 | Enrollment Date |
13 | Students |
14 |
15 |
16 |
17 | {statistics.map(statistic =>
18 |
19 | )}
20 |
21 |
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 |
13 | Last Name
14 | |
15 | First Name |
16 |
17 | Enrollment Date
18 | |
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 |
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