diff --git a/README.md b/README.md index c0da9e1..f1db350 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # register A web app for simplifying the entire registration process. Done as a final project for Olin.js Spring 2017. + +## To Run: +1) Clone this repository and navigate into it. +1) Run `npm install` to install packages. +1) Run `npm run build` to build the frontend bundled code from webpack. +1) Run `npm start` in a separate terminal window to run the backend server locally. +1) In your browser, go to localhost:3000/ to see the web app. + +The backend is run using nodemon, which should restart the server automatically when code changes as long as npm start is running. Similarly, the frontend is bundled using webpack, which should restart the server automatically as long as npm run build is running. Manually restart any of these commands if problems don't update correctly. \ No newline at end of file diff --git a/public/stylesheets/semester.css b/public/stylesheets/semester.css new file mode 100644 index 0000000..c215832 --- /dev/null +++ b/public/stylesheets/semester.css @@ -0,0 +1,4 @@ +.semesterheading { + font-weight: bold; + font-size: 12pt; +} \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 05a7802..f50b369 100644 --- a/routes/index.js +++ b/routes/index.js @@ -42,11 +42,18 @@ router.get('/logout', // register new user with local strategy router.post('/register', (req, res) => { Student.register(new Student({ username: req.body.username }), - req.body.password, (regErr, account) => { + req.body.password, (regErr, newAccount) => { if (regErr) { console.error(regErr); res.status(401).send(regErr.message); } else { + // hotfix. Redux demands these fields be filled, + // but they have no default values + const account = Object.assign(newAccount, { + name: 'Test User', + entryYear: '2001', + major: 'Mechanical Engineering', + }); account.save((saveErr) => { if (saveErr) { console.error(saveErr); @@ -82,6 +89,19 @@ router.post('/register', (req, res) => { }); }); +// update student's plan of study +router.post('/updateplan', (req, res) => { + Student.update( + { _id: req.user._id }, + { plannedCourses: req.body.plannedCourses }, + (err) => { + if (err) { + console.error(err); + } + res.json({ success: true }); + }); +}); + // get student completed courses router.get('/completedcourses', (req, res) => { res.json({ diff --git a/src/actions/actions.jsx b/src/actions/actions.jsx index 2e751dc..52f1e22 100644 --- a/src/actions/actions.jsx +++ b/src/actions/actions.jsx @@ -88,6 +88,13 @@ export const updateSuggestions = suggestions => ({ suggestions, }); +// Update plan button +export const updatePlanSuccess = data => ({ + type: 'UPDATE_PLAN_SUCCESS', + isSuccess: data.success, +}); + +// Login backend interaction // login or register action successful export const receiveUser = json => ({ type: 'RECEIVE_USER', @@ -160,6 +167,18 @@ export const register = (username, password) => ( } ); +// Save plan of study backend +export const updatePlan = plannedCourses => ( + (dispatch) => { + const data = { + plannedCourses, + }; + $.post('/updateplan', data) + .done(response => (dispatch(updatePlanSuccess(response)))) + .fail((err, status) => console.error(err, status)); + } +); + // update login form username export const updateUsername = username => ({ type: 'UPDATE_USERNAME', diff --git a/src/components/CoursePlanner.jsx b/src/components/CoursePlanner.jsx index ec795e1..7d87cac 100644 --- a/src/components/CoursePlanner.jsx +++ b/src/components/CoursePlanner.jsx @@ -4,6 +4,7 @@ import { Row, Col } from 'react-bootstrap'; import AddCourseDropdown from './AddCourseDropdown'; import SelectedCourse from './SelectedCourse'; import SearchFieldContainer from '../containers/SearchFieldContainer'; +import UpdatePlanContainer from '../containers/UpdatePlanContainer'; import NavPanel from './NavPanel'; import styles from '../../public/stylesheets/pages.css'; @@ -40,6 +41,7 @@ const CoursePlanner = ({ categories, otherCourses, onCourseSelect, onCourseRemov

Search All Courses

+ diff --git a/src/components/NavPanel.jsx b/src/components/NavPanel.jsx index 8cab4ef..656d71c 100644 --- a/src/components/NavPanel.jsx +++ b/src/components/NavPanel.jsx @@ -13,8 +13,8 @@ const NavPanel = ({ active }) => ( Create/Edit Plan of Study - - Other Stuff + + Plan by Semester diff --git a/src/components/Semester.jsx b/src/components/Semester.jsx index 663077e..6465f26 100644 --- a/src/components/Semester.jsx +++ b/src/components/Semester.jsx @@ -1,12 +1,13 @@ // A single semester rendered as an
  • . SemesterList contains these. import React, { PropTypes } from 'react'; import CourseBlockContainer from '../containers/CourseBlockContainer'; +import styles from './../../public/stylesheets/semester.css'; const Semester = ({ semester, courseList, connectDropTarget }) => ( // Indicate node should react to drop target events connectDropTarget(
  • - { semester }: + { semester }:
      {courseList.map(course => ( +const SemesterList = ({ fallSemesters, springSemesters }) => (

      Plan by Semester

        -
        - {semesters.map(semester => + + {/* col attributes resize/reorder for different size screens */} + + {fallSemesters.map(semester => + , + )} + + + +
        + {springSemesters.map(semester => , )} + +
        + +
      ); SemesterList.propTypes = { semesters: PropTypes.arrayOf(PropTypes.string).isRequired, + fallSemesters: PropTypes.arrayOf(PropTypes.string).isRequired, + springSemesters: PropTypes.arrayOf(PropTypes.string).isRequired, }; export default SemesterList; diff --git a/src/components/SemesterPlanPage.jsx b/src/components/SemesterPlanPage.jsx index 8a29b6d..c8f4ad7 100644 --- a/src/components/SemesterPlanPage.jsx +++ b/src/components/SemesterPlanPage.jsx @@ -1,11 +1,24 @@ // The page that holds the semester list. import React from 'react'; +import { Row, Col } from 'react-bootstrap'; import SemesterListContainer from '../containers/SemesterListContainer'; +import UpdatePlanContainer from '../containers/UpdatePlanContainer'; +import NavPanel from './NavPanel'; +import styles from '../../public/stylesheets/pages.css'; const SemesterPlanPage = () => ( -
      - -
      + + {/* col attributes resize/reorder for different size screens */} + + + + +
        + + +
      + +
      ); export default SemesterPlanPage; diff --git a/src/components/SemesterReserve.jsx b/src/components/SemesterReserve.jsx index cfedad2..1bb699b 100644 --- a/src/components/SemesterReserve.jsx +++ b/src/components/SemesterReserve.jsx @@ -1,11 +1,12 @@ import React, { PropTypes } from 'react'; import CourseBlockContainer from '../containers/CourseBlockContainer'; +import styles from './../../public/stylesheets/semester.css'; const SemesterReserve = ({ courseList, connectDropTarget }) => ( // Indicate node should react to drop target events connectDropTarget(
    • - Unassigned Courses: + Unassigned Courses:
        {courseList.map(course => ( + +); + +UpdatePlan.propTypes = { + onClick: PropTypes.func.isRequired, +}; + +export default UpdatePlan; diff --git a/src/containers/SemesterContainer.jsx b/src/containers/SemesterContainer.jsx index 50c68c6..8a8ef0c 100644 --- a/src/containers/SemesterContainer.jsx +++ b/src/containers/SemesterContainer.jsx @@ -3,8 +3,15 @@ import { DropTarget } from 'react-dnd'; import Semester from '../components/Semester'; const filterCourse = (semester, completedCourses, plannedCourses) => { - const compare = (a, b) => { - // sort alphabetically by registrarId + const compareByCourseId = (a, b) => { + // sort alphabetically by courseId (where the course code is stored in plannedCourses) + if (a.courseId.toUpperCase() < b.courseId.toUpperCase()) { + return -1; + } + return 1; + }; + const compareByRegistrarId = (a, b) => { + // sort alphabetically by registrarId (where the course code is stored in completedCourses) if (a.registrarId.toUpperCase() < b.registrarId.toUpperCase()) { return -1; } @@ -12,10 +19,11 @@ const filterCourse = (semester, completedCourses, plannedCourses) => { }; const filteredCompleted = completedCourses.filter(course => course.semester === semester, - ).sort(compare); + ).sort(compareByRegistrarId); + console.log(plannedCourses, 'plannedCourses'); const filteredPlanned = plannedCourses.filter(course => course.semester === semester, - ).sort(compare); + ).sort(compareByCourseId); return filteredPlanned.concat(filteredCompleted); }; diff --git a/src/containers/SemesterListContainer.jsx b/src/containers/SemesterListContainer.jsx index 7a73405..3f55bbb 100644 --- a/src/containers/SemesterListContainer.jsx +++ b/src/containers/SemesterListContainer.jsx @@ -15,8 +15,26 @@ const makeSemesterList = (entryYear) => { return semesterList; }; +const makeFallSemesterList = (entryYear) => { + const fallSemesterList = []; + for (let i = 0; i < 4; i += 1) { + fallSemesterList.push('Fall' + shortenYear(entryYear + i)); + } + return fallSemesterList; +}; + +const makeSpringSemesterList = (entryYear) => { + const springSemesterList = []; + for (let i = 0; i < 4; i += 1) { + springSemesterList.push('Spring' + shortenYear(entryYear + i + 1)); + } + return springSemesterList; +}; + const mapStateToProps = state => ({ semesters: makeSemesterList(state.Student.entryYear), + fallSemesters: makeFallSemesterList(state.Student.entryYear), + springSemesters: makeSpringSemesterList(state.Student.entryYear), }); const SemesterListContainer = connect( diff --git a/src/containers/UpdatePlanContainer.jsx b/src/containers/UpdatePlanContainer.jsx new file mode 100644 index 0000000..552a4bb --- /dev/null +++ b/src/containers/UpdatePlanContainer.jsx @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { updatePlan } from '../actions/actions'; +import UpdatePlan from '../components/UpdatePlan'; + +const mapStateToProps = state => ({ + plannedCourses: state.Student.plannedCourses, +}); + +const mergeProps = (stateProps, dispatchProps) => { + const { plannedCourses } = stateProps; + const { dispatch } = dispatchProps; + + return { + onClick: (e) => { + e.preventDefault(); + dispatch(updatePlan(plannedCourses)); + }, + }; +}; + +const UpdatePlanContainer = connect( + mapStateToProps, + null, + mergeProps, +)(UpdatePlan); + +export default UpdatePlanContainer; diff --git a/src/reducers/LoginReducer.jsx b/src/reducers/LoginReducer.jsx deleted file mode 100644 index 4f34f95..0000000 --- a/src/reducers/LoginReducer.jsx +++ /dev/null @@ -1,58 +0,0 @@ -const initialState = { - username: '', - password: '', - name: '', - id: '', - entryYear: 0, - major: '', - plannedCourses: [], - completedCourses: [], - allCourses: [], - loggedIn: false, - loginError: '', - registerError: '', -}; - -const LoginReducer = (state = initialState, action) => { - switch (action.type) { - case 'UPDATE_USERNAME': - return Object.assign({}, state, { - username: action.username, - }); - case 'UPDATE_PASSWORD': - return Object.assign({}, state, { - password: action.password, - }); - case 'RECEIVE_USER': - return Object.assign({}, state, { - username: action.username, - name: action.name, - id: action.id, - entryYear: action.entryYear, - major: action.major, - plannedCourses: action.plannedCourses, - completedCourses: action.completedCourses, - allCourses: action.courses, - loggedIn: true, - }); - case 'LOGOUT_USER': - return Object.assign({}, state, { - loggedIn: false, - }); - case 'CHANGE_SEMESTER': { - const isCourseMatch = course => course.courseId === action.courseId; - const index = state.plannedCourses.findIndex(isCourseMatch); - const course = state.plannedCourses.find(isCourseMatch); - course.semester = action.newSemester; - const updatedPlanned = state.plannedCourses; - updatedPlanned[index] = course; - return Object.assign({}, state, { - plannedCourses: updatedPlanned, - }); - } - default: - return state; - } -}; - -export default LoginReducer; diff --git a/src/reducers/ProgressTrackerReducer.jsx b/src/reducers/ProgressTrackerReducer.jsx deleted file mode 100644 index 7af03f7..0000000 --- a/src/reducers/ProgressTrackerReducer.jsx +++ /dev/null @@ -1,90 +0,0 @@ -const initialState = { - genreqs: 0, - majorreqs: 0, -}; - -function sum(list) { - let total = 0; - for (let i = 0, len = list.length; i < len; i += 1) { - total += list[i].credits; - } - return total; -} - -const ProgressTrackerReducer = (state = initialState, action) => { - const mathCourses = []; - const engrCourses = []; - const sciCourses = []; - const ahseCourses = []; - - switch (action.type) { - case 'GET_COMPLETED_COURSES': { - const data = action.data; - let genreqs = data.map(a => a.generalRequirements[0]); - genreqs = genreqs.filter(n => n !== undefined); - let majorreqs = data.map(a => a.otherRequirements[0]); - majorreqs = majorreqs.filter(n => n !== undefined); - - for (let i = 0; i < data.length; i += 1) { - switch (data[i].registrarId.substring(0, 3)) { - case 'MTH': - mathCourses.push({ title: data[i].title, credits: data[i].credits }); - break; - case 'ENG': - engrCourses.push({ title: data[i].title, credits: data[i].credits }); - break; - case 'AHS': - ahseCourses.push({ title: data[i].title, credits: data[i].credits }); - break; - case 'SCI': - sciCourses.push({ title: data[i].title, credits: data[i].credits }); - break; - default: - break; - } - } - - const mathTotal = sum(mathCourses); - const engrTotal = sum(engrCourses); - const ahseTotal = sum(ahseCourses); - const sciTotal = sum(sciCourses); - - // converts progress into a string that can be used by - // the progress bar in the ProgressTracker component - const mathSciPercNum = Math.round((mathTotal + sciTotal) * 3.33); - const mathSciPerc = mathSciPercNum.toString().concat('%'); - const engrPercNum = Math.round(engrTotal * 2.17); - const engrPerc = engrPercNum.toString().concat('%'); - const ahsePercNum = Math.round(ahseTotal * 3.57); - const ahsePerc = ahsePercNum.toString().concat('%'); - const genReqsPercNum = Math.round(genreqs.length * 6.25); - const genReqsPerc = genReqsPercNum.toString().concat('%'); - const majorReqsPercNum = Math.round(majorreqs.length * 14.28); - const majorReqsPerc = majorReqsPercNum.toString().concat('%'); - - - return Object.assign({}, state, { - genreqs, - majorreqs, - mathCourses, - engrCourses, - sciCourses, - ahseCourses, - mathTotal, - engrTotal, - ahseTotal, - sciTotal, - mathSciPerc, - engrPerc, - ahsePerc, - genReqsPerc, - majorReqsPerc, - }); - } - default: { - return state; - } - } -}; - -export default ProgressTrackerReducer; diff --git a/src/reducers/RegisterReducer.jsx b/src/reducers/RegisterReducer.jsx deleted file mode 100644 index 8f87f2f..0000000 --- a/src/reducers/RegisterReducer.jsx +++ /dev/null @@ -1,21 +0,0 @@ -const initialState = { - username: '', - password: '', -}; - -const RegisterReducer = (state = initialState, action) => { - switch (action.type) { - case 'UPDATE_REGISTER_USERNAME': - return Object.assign({}, state, { - username: action.username, - }); - case 'UPDATE_REGISTER_PASSWORD': - return Object.assign({}, state, { - password: action.password, - }); - default: - return state; - } -}; - -export default RegisterReducer; diff --git a/src/reducers/ReqsReducer.jsx b/src/reducers/ReqsReducer.jsx deleted file mode 100644 index fbacd39..0000000 --- a/src/reducers/ReqsReducer.jsx +++ /dev/null @@ -1,44 +0,0 @@ -const initialState = { - isGenReq: false, - isMajorReq: false, - isMathsci: false, - isAhse: false, - isEngr: false, -}; - -const ReqsReducer = (state = initialState, action) => { - switch (action.type) { - case 'UPDATE_GEN_REQ': - return Object.assign({}, state, { - isGenReq: true, - }); - case 'UPDATE_MAJOR_REQ': - return Object.assign({}, state, { - isMajorReq: true, - }); - case 'UPDATE_MATHSCI_REQ': - return Object.assign({}, state, { - isMathsci: true, - }); - case 'UPDATE_AHSE': - return Object.assign({}, state, { - isAhse: true, - }); - case 'UPDATE_ENGR': - return Object.assign({}, state, { - isEngr: true, - }); - case 'RESET_REQ': - return Object.assign({}, state, { - isGenReq: false, - isMajorReq: false, - isMathsci: false, - isAhse: false, - isEngr: false, - }); - default: - return state; - } -}; - -export default ReqsReducer; diff --git a/src/reducers/RequirementsReducer.jsx b/src/reducers/RequirementsReducer.jsx deleted file mode 100644 index 4cde1f9..0000000 --- a/src/reducers/RequirementsReducer.jsx +++ /dev/null @@ -1,18 +0,0 @@ -const initialState = { - generalRequirements: [], - majorRequirements: [], -}; - -const RequirementsReducer = (state = initialState, action) => { - switch (action.type) { - case 'RECEIVE_REQUIREMENTS': - return Object.assign({}, state, { - generalRequirements: action.generalRequirements, - majorRequirements: action.majorRequirements, - }); - default: - return state; - } -}; - -export default RequirementsReducer; diff --git a/src/reducers/SettingsPageReducer.jsx b/src/reducers/SettingsPageReducer.jsx deleted file mode 100644 index 3f1d59c..0000000 --- a/src/reducers/SettingsPageReducer.jsx +++ /dev/null @@ -1,36 +0,0 @@ -const initialState = { - settings: [ - { - name: 'setting1', - checked: false, - }, - { - name: 'setting2', - checked: false, - }, - { - name: 'setting3', - checked: false, - }, - ], -}; - -const SettingsPageReducer = (state = initialState, action) => { - switch (action.type) { - case 'TOGGLE_SETTING': - return Object.assign({}, state, { - settings: state.settings.map((setting) => { - if (setting.name === action.name) { - return Object.assign({}, setting, { - checked: !setting.checked, - }); - } - return setting; - }), - }); - default: - return state; - } -}; - -export default SettingsPageReducer;