├── README.md ├── backend ├── app.js ├── bin │ └── www ├── package-lock.json ├── package.json ├── public │ └── stylesheets │ │ └── style.css ├── routes │ ├── index.js │ ├── public.js │ ├── student.js │ └── utilities.js ├── utils │ ├── auth0-variables.js │ ├── database-mysql.js │ ├── database-mysql2.js │ ├── database-variables.js │ ├── jwtcheck.js │ └── user.js └── views │ ├── error.jade │ ├── index.jade │ └── layout.jade ├── frontend ├── package-lock.json ├── package.json ├── public │ ├── css │ │ ├── material-dashboard.css │ │ ├── material-dashboard.css.map │ │ └── material-dashboard.min.css │ ├── favicon.ico │ ├── index.html │ ├── js │ │ ├── bootstrap-material-design.js │ │ ├── bootstrap-material-design.js.map │ │ ├── bootstrap-material-design.min.js │ │ ├── core │ │ │ ├── jquery.min.js │ │ │ └── popper.min.js │ │ ├── material-dashboard.js │ │ └── plugins │ │ │ ├── arrive.min.js │ │ │ ├── bootstrap-notify.js │ │ │ ├── bootstrap.min.js │ │ │ ├── chartist.min.js │ │ │ ├── demo.js │ │ │ ├── material.min.js │ │ │ └── perfect-scrollbar.jquery.min.js │ └── manifest.json └── src │ ├── Auth │ ├── Auth.js │ └── auth0-variables.js │ ├── api │ ├── public.js │ ├── sendRequest.js │ └── student.js │ ├── components │ ├── Callback.js │ ├── NaviBar.js │ ├── Notification.js │ ├── SideBar.js │ ├── appointments │ │ └── AppointmentCard.js │ ├── main │ │ └── MainCard.js │ ├── student │ │ ├── InfoCard.js │ │ ├── ProfileCard.js │ │ ├── Student.js │ │ └── favicon.ico │ ├── template │ │ ├── StopSelect.js │ │ └── Table.js │ └── timetable │ │ └── TimeTable.js │ ├── history.js │ ├── index.css │ ├── index.js │ └── registerServiceWorker.js ├── imgs ├── CampusRide-EERD.png ├── MetaData-Appointment.png ├── MetaData-AutoTimeTable.png ├── MetaData-Schedule.png ├── MetaData-Vehicle.png ├── ScreenShot-AppointmentCard.png ├── ScreenShot-AutoAppointment.png ├── ScreenShot-AutoTimeTable.png ├── ScreenShot-CartAppointment.png ├── ScreenShot-CartRideTable.png ├── ScreenShot-LoginWindow.png ├── ScreenShot-ProfileCard.png ├── use-case.jpg └── use-case.pdf └── mysql-database ├── Dump-CampusRide.sql └── test-data-generator └── schedule.py /README.md: -------------------------------------------------------------------------------- 1 | # CampusRide-React-Express-MySQL 2 | The project is an full stack implementation including database design for the campus ride appointment system of golf cart and campus auto at UNC Charlotte. 3 | 4 | The main structure of the repository is as below: 5 | 6 | . 7 | ├── Frontend # Frontend directory: React 4 + BootStrap 4 8 | │ ├── public # Static file directory: contains css, bootstrap, javascript for the views 9 | │ ├── ... 10 | │ ├── src # React file directory 11 | │ ├── api # Http api communicate to backend API 12 | │ ├── public.js # Public api without user authentification 13 | │ ├── student.js # Private api require user authentification 14 | │ ├── Auth # Auth0 user authentification 15 | │ ├── auth0-variables.js # The configuration of Auth0: ** you should setup your own auth0 server here ** 16 | │ ├── Auth.js # Authentification api via Auth0 17 | │ ├── components 18 | │ ├── appointments # Appointment page 19 | │ ├── main # Main page 20 | │ ├── student # Profile page 21 | │ ├── timetable # Auto & cart timetable page 22 | │ ├── Callback.js # Callback page: store the authentification token 23 | │ ├── NaviBar.js # Navigation component 24 | │ ├── SideBar.js # SideBar component 25 | │ ├── ... 26 | │ ├── index.js # The root react component: react application entry point 27 | │ ├── 28 | │ ├── package.json # Required node js modules 29 | ├── Backend # Backend API directory: Node.js + Express 30 | │ ├── public # Public files: no need for this app, just express generator legacy 31 | │ ├── ... 32 | │ ├── routes # Main API directory which communicates to the frontend api 33 | │ ├── public.js # Public API: receive queries from frontend API and send response data back 34 | │ ├── Student.js # Authentification required API: receive and process requests from frontend API 35 | │ ├── utilities.js # SQL helper functions 36 | │ ├── utils # Database API & Authentification directory 37 | │ ├── auth0-variables.js # Auth0 configuration: ** you should setup your own auth0 server here ** 38 | │ ├── database-variables.js # MySQL configuration: ** you should setup your own mysql server here ** 39 | │ ├── database-mysql.js # Synchronized mysql api 40 | │ ├── database-mysql2.js # Asynchronized mysql2 api 41 | │ ├── jwtcheck.js # JWT authentification middleware 42 | │ ├── user.js # Auth0 & MySQL related api 43 | │ ├── view # Jade template directory: little need for this app, just express generator legacy and debug use 44 | │ ├── package.json # Required node js modules 45 | ├── mysql-database # MySQL database directory 46 | │ ├── Dump-CampusRide.sql # Mysql dump file 47 | │ ├── test-data-generator 48 | │ ├── schedule.py # Python helper function for generate test data for auto and cart schedule 49 | └── README.md 50 | 51 | ## Index 52 | * [Setup and Run](#setup-and-run) 53 | * [Development Starting Point and Framework](#development-starting-point-and-framework) 54 | * [Business Rules and Assumptions](#business-rules-and-assumptions) 55 | * [Web UI](#web-ui) 56 | * [EERD](#eerd) 57 | * [Data Dictionary](#data-distionary) 58 | * [SQL Implementation Details](#sql-implementation-details) 59 | * [Stored Procedure](#stored-procedure) 60 | * [Trigger](#trigger) 61 | * [CRUI API](#crui-api) 62 | * [View](#view) 63 | * [Indexes](#indexes) 64 | * [ToDo(Future Work)](#todo) 65 | * [References](#references) 66 | 67 | ## Setup and Run 68 | 69 | * Node.js enviroment setup 70 | 71 | Follow the link https://nodejs.org/en/ for the installation. 72 | 73 | * MySQL database setup and run 74 | 75 | Using the Dump-CampusRide.sql file to import database in your MySQL server. 76 | For the bussiness rule concern, the web UI only supports the query and update for the appointments after current date. And the database dump file only generate the schedule data up to May 2018. In order to support the full functionalities of the application after that time, you should generate more test data for the appropriate dates. Reference to the schedule.py file under test-data-generator directory for the use to generate mysql syntaxes to insert data. 77 | Start your MySQL server: 78 | ```bash 79 | mysqld_safe -u 'mysql' 80 | ``` 81 | Edit ./backend/utils/database-variables.js file with your MySQL server configuration. 82 | ```javascript 83 | // edit ./backend/utils/database-variables.js 84 | const MYSQL_CONFIG={ 85 | host : 'yourhosturl', 86 | user : 'yourusername', 87 | password : 'yourpassword', 88 | database : 'CampusRide' 89 | } 90 | module.exports = {MYSQL_CONFIG} 91 | 92 | ``` 93 | 94 | * Auth0 service setup 95 | 96 | The authentification of the app use Auth0 based JWT authentification method. There is a free tier for use Auth0, check out the website (https://auth0.com/) for more details. After setting up an api/client of on Auth0 account, edit ./backend/utils/auth0-variables.js and ./frontend/src/Auth/auth0-variables.js with your Auth0 configuration. For the backend compatibility of user profile process, it is recommended using the connection for Google account. 97 | 98 | ```javascript 99 | // edit ./backend/utils/auth0-variables.js 100 | const domain = 'yourauth0-domain.auth0.com'; 101 | 102 | module.exports = {domain}; 103 | ``` 104 | 105 | ```javascript 106 | // edit ./frontend/src/Auth/auth0-variables.js 107 | export const AUTH_CONFIG = { 108 | domain: 'yourauth0-domain.auth0.com', 109 | clientId: 'your-auth0-clientId', 110 | callbackUrl: 'http://localhost:3001/callback', // you need set call back url at Auth0 111 | audience: 'your-audience' 112 | } 113 | ``` 114 | 115 | * Backend setup and run 116 | From the project root directory, 117 | 118 | ```bash 119 | cd backend 120 | npm install # install node pakages for backend 121 | node ./bin/www # run backend server on default port 3000 122 | 123 | ``` 124 | 125 | The backend api will talk to MySQL server and listen on port 3000 for data fetching requests. 126 | 127 | * Frontend setup and run 128 | From the project root directory, 129 | ```bash 130 | cd frontend 131 | npm install # install node pakages 132 | PORT=3001 npm start # start frontend server on port 3001 133 | ``` 134 | Open your browser, enter and open url https://yourserverurl:3001 (http://localhost:3001 if you run on your local machine) to see the main page of the application. 135 | 136 | ## Development Starting Point and Framework 137 | 138 | * Frontend (React 4 + Bootstrap 4) 139 | 140 | For the simplicity setup about React, I use [Create React App](https://github.com/facebook/create-react-app) framework as the starting point of the project. 141 | 142 | For the bootstrap and html components, I use the theme/template [Light Bootstrap Dashboard](https://www.creative-tim.com/product/light-bootstrap-dashboard) for the basic structure design. 143 | 144 | * Backend (Node.js + Express) 145 | 146 | I use [express application generator](https://expressjs.com/en/starter/generator.html) framework as the starting point of the backend api. Since the backend server serves like a data API, there is no need for some view template setup in the initial generating framework. 147 | 148 | 149 | ## Business Rules and Assumptions 150 | 151 | The system support three kind of user to login, *student*, *driver* and *manager* (admin user). The student can make/change appointment for two kinds of rides, auto ride and golf cart ride. The auto ride has fixed timetable and stops which is free while the golf cart ride can have customized pick-up time and do not have spercific drop off location with more flexibility thus it is chargeable. The driver would be assigned to specific vehicle (including auto and cart) on specific date by manager. 152 | 153 | ### Full Use Case 154 | 155 | Student can register and login to the system to make the appointment for golf cart or auto ride. After the ride, they can give the feedback and rate the driver or the ride. Manager (admin user) can login to make schedule for the auto line or golf cart to drivers on specific date. Driver can login to the system to check the schedule and finish the cart ride trip by filling the billing information. 156 | 157 |

158 | use case 159 |

160 | 161 | 162 | ### Implemented Parts 163 | 164 | For the database, it implements the whole part of the full use case including 12 tables with generalizaiton/specialization relationship. In this web application, due to the time limitation, I only implemented some key functionalities of the student user part, which includes **looking up auto/cart schedule timetable, registration, log in, log out, make/look up/cancel appointments, look up/update profile**. 165 | 166 | ### More About Auto Ride 167 | 168 | The auto ride has scheduled lines and timetable. Each auto with associated timetable would be assigned to a driver on spercific working day by the manager. After manager's schedule, student can look up and see the scheduled auto ride timetable by choosing the pick up stop, drop off stop and date. Right now, for the test data (which is included in SQL dump file), I use the existing [UNCC Next Ride](http://nextride.uncc.edu/). The stop information is refrenced from https://pats.uncc.edu/transportation/transportation-tracking. The routes in the next ride, gold, silver and green line are corresponding to line 0, 1, 2 in SQL route table. 169 | 170 | ### More About Cart Ride 171 | 172 | Cart ride and have customizable pick up time and do not need set the spercific drop off location. Thus, it is more flexible and maybe chargeable. For the pick up communication concern, it should have spercific pick up location at the time making appointment. Each scheduled cart with assigned driver could only have one appointment within half hour. 173 | 174 | ## Web UI 175 | 176 | The frontend use React 4 + Bootstrap 4. The view part is based on the theme/template from Creative Tim's [Light Bootstrap Dashboard](https://www.creative-tim.com/product/light-bootstrap-dashboard). 177 | 178 | ### Public/Guest Time Table Look Up 179 | 180 | **Without login** , the guest user can look up the scheduled auto time table by choosing date, pick up stop and drop off stop. The auto time table shows the scheduled auto ride with spercific pick up time and drop off time. It only shows the auto ride with seats lelf. Also, by choosing the date and pick up stop, the guest user can also look up the available cart with assigned driver. Each golf cart with assigned driver can only have one appointment within half hour. 181 | 182 |

183 | Auto Time Table 184 | Cart Time Table 185 |

186 | 187 | ### User Authentification 188 | 189 | In order to have full functionalities, the user is required to login. The current emplimentation of user authentification system use JWT based [Auth0](https://auth0.com/). For better backend compatibility, I suggest use google connection besides the native registration and login support. 190 | 191 |

192 | Auth0 Login Window 193 |

194 | 195 | ### Manage Appointments (create, look up and cancel) 196 | 197 | **After login**, 198 | * the user can *make* new appointments: 199 | 200 |

201 | Make Auto Appointment 202 | Make Cart Appointment 203 |

204 | 205 | * *look up* and *cancel* appointment information: 206 | 207 |

208 | Make Auto Appointment 209 |

210 | 211 | ### Manage Profile 212 | 213 | **After login**, the user can look up the user profile stored in the system. For the user login via google account, the system could automaticaly retrieve the user profile information, e.g. first name, last name and email from google account. 214 | 215 |

216 | Make Auto Appointment 217 |

218 | 219 | 220 | ## EERD 221 | 222 | The database design contains 13 tables. It has the following assumption/rules: 223 | 224 | * Each route(or Line) has a fixed sequence of stops and the elapse time from the first stop 225 | 226 | * Each auto has fixed timetable with a line and spercific staring time 227 | 228 | * Each auto would be assigned to a driver by manager which represented by the Schedule table in the design 229 | 230 | * Each cart would be assigned to a driver by manager which also included in the Schedule table in the design 231 | 232 | * Cart ride appointment is chargeable and has custermizable pick up time without spercific drop off location 233 | 234 | * Auto ride appointment has pick up stop and drop off stop along with the spercific route time schedule 235 | 236 | 237 |

238 | Make Auto Appointment 239 |

240 | 241 | ### Generalization / Specialization 242 | 243 | In the design, Vehicle is the **generalization** for the Cart and Auto. CartRide and AutoRide are **specialization** for the Appointment with common attribute, *apt_id, student_id, schedule_id, rating, comment, pick_up_time and pick_up_stop*. 244 | 245 | ## Data Dictionary 246 | 247 | ### Vehicle and Auto Time Table 248 | 249 |

250 | MetaData Vehicle 251 | MetaData AutoTimeTable 252 |

253 | 254 | ### Driver and Schedule 255 | 256 |

257 | MetaData Schedule 258 |

259 | 260 | ### Student and Appointment 261 | 262 |

263 | MetaData Appointment 264 |

265 | 266 | ## SQL Implementation Details 267 | 268 | ### Stored Procedure 269 | 270 | Since Auto Ride and Cart Ride are fully participated in the specialization relationship with Appointment and every auto appointment should have selected drop off stop at the time making it, it is convenient and good for abstraction to provide insert_auto_ride store procedure which hides the implementation details about the specialization relationship to application program. The *insert_auto_ride* procedure firstly insert the auto appointment records to the Appointment table, then insert the additional attributes into the AutoRide table using the *last_insert_id()* function. 271 | 272 | ```mysql 273 | CREATE DEFINER=`root`@`localhost` PROCEDURE `insert_auto_ride`(stu_id int, sche_id int, pik_time time, pik_stop int, drop_stop int) 274 | begin 275 | insert into Appointment(student_id, schedule_id, type, pick_up_time, pick_up_stop) 276 | values(stu_id, sche_id, 'auto', pik_time, pik_stop); 277 | set @apt_id = (select last_insert_id()); 278 | insert into AutoRide(apt_id, drop_off_stop) 279 | values(@apt_id, drop_stop); 280 | end 281 | ``` 282 | 283 | ### Trigger 284 | 285 | Auto and Cart are fully participated in the generalization relationship with Vehicle. There are foreign keys *vehicle_id* in both table referencing to Vehicle table. Therefor, it is natural to create two triggers before the *insert* apperation on Auto and Cart Tables. 286 | 287 | ```mysql 288 | CREATE DEFINER=`root`@`localhost` TRIGGER `CampusRide`.`Auto_BEFORE_INSERT` 289 | before INSERT ON `Auto` 290 | FOR EACH ROW 291 | BEGIN 292 | insert into Vehicle 293 | values(new.vehicle_id, 'auto'); 294 | END 295 | 296 | CREATE DEFINER=`root`@`localhost` TRIGGER `CampusRide`.`Cart_BRFORE_INSERT` 297 | before INSERT ON `Cart` 298 | FOR EACH ROW 299 | BEGIN 300 | insert into Vehicle 301 | values(new.vehicle_id, 'cart'); 302 | END 303 | ``` 304 | 305 | ### CRUD API 306 | 307 | * Public API (without user authentification) 308 | * **Read** Auto Time Table: 309 | ```javascript 310 | // in ./backend/routes/public.js 311 | /* version one 312 | let sql = 'select schedule_id, line_id, maker, capacity, year, pick_up_time, drop_off_time '+ 313 | ' from StopTimeTable, Schedule, Auto '+ 314 | ' where pick_up_stop='+pick_up_stop+ 315 | ' and drop_off_stop='+drop_off_stop+ 316 | ' and date='+date+ 317 | ' and Schedule.vehicle_id=StopTimeTable.vehicle_id '+ 318 | ' and Schedule.vehicle_id=Auto.vehicle_id'+ 319 | ' order by pick_up_time'; 320 | */ 321 | // version three: support seats_left query 322 | let AppointmentTable = '(select Appointment.apt_id, Appointment.schedule_id, stoptimetable.vehicle_id, stoptimetable.pick_up_time, drop_off_time '+ 323 | ' from Appointment, AutoRide, Schedule, stoptimetable '+ 324 | ' where '+ 325 | ' Appointment.type=\'auto\' and date='+date+ 326 | ' and Appointment.apt_id=AutoRide.apt_id '+ 327 | ' and Schedule.schedule_id=Appointment.schedule_id '+ 328 | ' and stoptimetable.vehicle_id = Schedule.vehicle_id '+ 329 | ' and stoptimetable.drop_off_stop = AutoRide.drop_off_stop '+ 330 | ' and stoptimetable.pick_up_stop = Appointment.pick_up_stop) AppointmentTable '; 331 | let SelectedStopTimeTable = '(select line_id, vehicle_id, pick_up_time, drop_off_time from stoptimetable '+ 332 | ' where pick_up_stop='+pick_up_stop+' and drop_off_stop='+drop_off_stop+') SelectedStopTimeTable'; 333 | let LeftJoinTable = '(select SelectedStopTimeTable.*, apt_id from '+ 334 | SelectedStopTimeTable + ' left join '+ AppointmentTable + 335 | ' on SelectedStopTimeTable.vehicle_id=AppointmentTable.vehicle_id '+ 336 | ' and not SelectedStopTimeTable.pick_up_time>AppointmentTable.drop_off_time '+ 337 | ' and not SelectedStopTimeTable.drop_off_timeapt_count '+ 343 | ' order by pick_up_time'; 344 | ``` 345 | 346 | * **Read** Auto Ride Table: 347 | ```javascript 348 | // in ./backend/routes/public.js 349 | // As requirement, each auto ride with assigned driver can only have one appointment within 0.5 hour 350 | const datetime = moment(req.body.datetime); 351 | let date = datetime.format('YYYY-MM-DD'); 352 | date = '\''+date+'\''; 353 | let time1 = datetime.subtract(0.5,'hours').format('hh:mm') 354 | time1 = '\'{}\''.format(time1); 355 | let time2 = datetime.add(1, 'hours').format('hh:mm'); 356 | time2 = '\'{}\''.format(time2); 357 | let sql = 358 | ' select schedule_id, Driver.driver_id, first_name as driver_first_name, last_name as driver_last_name, Cart.vehicle_id as cart_id, wheelchair_acce as accessibility from Schedule, Cart, Driver '+ 359 | ' where date={} '.format(date)+ 360 | ' and Cart.vehicle_id=Schedule.vehicle_id and Driver.driver_id=Schedule.driver_id '+ 361 | ' and not exists '+ 362 | ' (select * from Appointment where schedule_id=Schedule.schedule_id '+ 363 | ' and pick_up_time>{} and pick_up_time<{})'.format(time1, time2); 364 | ``` 365 | 366 | * API Required Authentification 367 | * **Create** Appointment: 368 | ```javascript 369 | // in ./backend/routes/student.js 370 | if(req.body.pick_up_stop&&req.body.pick_up_time) 371 | req.body.student_id = req.user.student_id; 372 | // auto ride: call stored procedure 373 | if(req.body.drop_off_stop){ 374 | let sql = 'call insert_auto_ride('+ 375 | '{student_id}, {schedule_id}, \'{pick_up_time}\', {pick_up_stop}, {drop_off_stop})'.format(req.body); 376 | return update_data(sql)(req, res, next); 377 | }// private ride 378 | else{ 379 | let sql = 'insert into Appointment(student_id, schedule_id, type, pick_up_time, pick_up_stop)'+ 380 | ' values({student_id}, {schedule_id}, \'cart\', \'{pick_up_time}\', {pick_up_stop})'.format(req.body); 381 | return update_data(sql)(req, res, next); 382 | } 383 | ``` 384 | * **Update** Profile: 385 | ```javascript 386 | // in ./backend/routes/student.js 387 | let sql='update Student set first_name=\'{first_name}\', last_name=\'{last_name}\', disabled_injured={disabled_injured}, cellphone=\'{cellphone}\' '.format(req.body); 388 | sql+=' where student_id='+req.user.student_id; 389 | ``` 390 | * **Delete** Appointment: 391 | ```javascript 392 | // in ./backend/routes/student.js 393 | process_data = async (req, res, next)=>{ 394 | if(req.body.apt_ids){ 395 | const {findUserByAppointment} = require('../utils/user.js'); 396 | let student_id; 397 | let sql='delete from Appointment where '; 398 | let index = 0; 399 | for(let apt_id of req.body.apt_ids){ 400 | student_id = await findUserByAppointment(apt_id); 401 | if(student_id!=req.user.student_id){ 402 | res.status(401).send(); // unauthorized error 403 | return ; 404 | } 405 | if(index===0) sql+=' apt_id='+apt_id; 406 | else sql += ' or apt_id='+apt_id; 407 | index+=1; 408 | } 409 | if(index==0) return next(); 410 | return update_data(sql)(req, res, next); 411 | } 412 | next(); 413 | } 414 | ``` 415 | In order to make sure the cancelation appointments belong to the requested user, e.g. malicious user may send manually crafted http request to backend API with appointment not made by himself, it checks every appointment's owner before cancellation by call *findUserByAppointment* function. 416 | 417 | * For more **Read** API for authentification users, e.g. read appointments, check out *./backend/routes/student.js* file. 418 | 419 | 420 | 421 | ### View 422 | 423 | Each auto is associated with certain lines and start time in TimeTable. The frequent query about the auto time table should should support with certain pick up stop and drop off stop. Thus, creating a *View* for *stoptimetable* can simplify the query SQL. Since there are seldom *update* or *delete* operations on the associated tables, *TimeTable* and *Route*, there is little overhead for maintaining the view. Thus, it is much reasonable to create the view here. 424 | 425 | ```mysql 426 | CREATE VIEW `stoptimetable` AS 427 | SELECT 428 | `linetable`.`line_id` AS `line_id`, 429 | `timetable`.`vehicle_id` AS `vehicle_id`, 430 | ADDTIME(`timetable`.`start_time`, 431 | MAKETIME(0, `linetable`.`time1`, 0)) AS `pick_up_time`, 432 | `pick_up_stop` AS `pick_up_stop`, 433 | ADDTIME(`timetable`.`start_time`, 434 | MAKETIME(0, `linetable`.`time2`, 0)) AS `drop_off_time`, 435 | `linetable`.`drop_off_stop` AS `drop_off_stop` 436 | FROM 437 | (((SELECT 438 | `t1`.`line_id` AS `line_id`, 439 | `t1`.`time1` AS `time1`, 440 | `t2`.`time2` AS `time2`, 441 | `t1`.`stop_id1` AS `pick_up_stop`, 442 | `t2`.`stop_id2` AS `drop_off_stop` 443 | FROM 444 | (((SELECT 445 | `route`.`line_id` AS `line_id`, 446 | `route`.`stop_id` AS `stop_id1`, 447 | `route`.`elapse_time` AS `time1` 448 | FROM 449 | `campusride`.`route`)) `T1` 450 | JOIN (SELECT 451 | `route`.`line_id` AS `line_id`, 452 | `route`.`stop_id` AS `stop_id2`, 453 | `route`.`elapse_time` AS `time2` 454 | FROM 455 | `route`) `T2` ON ((`t1`.`line_id` = `t2`.`line_id`))) 456 | WHERE 457 | (`t1`.`time1` < `t2`.`time2`))) `LineTable` 458 | JOIN `timetable` ON ((`linetable`.`line_id` = `timetable`.`line_id`))) 459 | 460 | ``` 461 | 462 | ### Indexes 463 | 464 | In the MySql implementation, each foreign key should associated with a foreign key index. Since all the queries in *where* and *join* condition we used in [CRUD API](#crud-api) are key or foreign key attributes, there is no need for supplementary indexes created for the optimization. It is indeed better to create indexes for *pick_up_stop* and *drop_off_stop* columns on view table *stoptimetable*, but it seems that the views in MySQL is not materializated and can not create indexes on them. 465 | 466 | ## ToDo 467 | 468 | * Optimize Auto Time Table Query: 469 | 470 | As can be seen in the CRUD API section, it much complex even with the *stoptimetable* view created. Moreover, it is the most frequent query in the application. Thus, some denormalization and redundant attribute (e.g. drop off time) should made for efficiency. 471 | 472 | * Develop Manager Web UI and Backend API: 473 | Right now, in order maintain the usage of the application for test or other use, the schedule can only be made directly to the database. In the [./mysql-database/test-data-generator/schedule.py](https://github.com/qychen13/CampusRide-React-Express-MySQL/blob/master/mysql-database/test-data-generator/schedule.py), it contains the script to generate MySQL syntax for the new schedule. 474 | 475 | ## References 476 | 477 | * Javascript 478 | * [ES6](https://developer.mozilla.org/en-US/docs/Web/JavaScript): complete tutorials about javascript including ES6 features 479 | 480 | * Frontend 481 | * [React Official Documentation](https://reactjs.org/): the well organized official documentation and tutorial 482 | * [Create React App](https://github.com/facebook/create-react-app): a very good quick starting point without a lot of setup about WebPack, Babel 483 | * [Creative Tim](https://www.creative-tim.com/): the well designed bootstrap themes/templates for views 484 | 485 | * Backend 486 | * [Express JS](https://expressjs.com/): the most popular node.js web module 487 | * [Express Generator](https://expressjs.com/en/starter/generator.html): a very good quick starting point with well organized development structure 488 | 489 | * Auth0 490 | * [backend node.js setup](https://auth0.com/docs/quickstart/webapp/nodejs/01-login) 491 | * [frontend react setup](https://auth0.com/docs/quickstart/spa/react/01-login) 492 | 493 | 494 | 495 | 496 | 497 | -------------------------------------------------------------------------------- /backend/app.js: -------------------------------------------------------------------------------- 1 | var createError = require('http-errors'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | var cookieParser = require('cookie-parser'); 5 | var logger = require('morgan'); 6 | const {findOrCreateUser} = require('./utils/user.js'); 7 | 8 | var studentRouter = require('./routes/student'); 9 | var publicRouter = require('./routes/public'); 10 | 11 | var app = express(); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'jade'); 16 | 17 | //Database connection 18 | const {mysqlConnection} = require('./utils/database-mysql.js') 19 | 20 | app.use(function(req, res, next){ 21 | res.locals.connection = mysqlConnection; 22 | next(); 23 | }); 24 | 25 | app.use(logger('dev')); 26 | app.use(express.json()); 27 | app.use(express.urlencoded({ extended: false })); 28 | app.use(cookieParser()); 29 | app.use(express.static(path.join(__dirname, 'public'))); 30 | 31 | // auth0 setup 32 | const jwtCheck = require('./utils/jwtCheck.js'); 33 | // for authentification debug use 34 | /*app.use(function(req, res, next){ 35 | console.log(req.headers.authorization); 36 | next(); 37 | })*/ 38 | 39 | app.use('/student', jwtCheck, findOrCreateUser, studentRouter); 40 | app.use('/public', publicRouter); 41 | 42 | // catch 404 and forward to error handler 43 | app.use(function(req, res, next) { 44 | next(createError(404)); 45 | }); 46 | 47 | // error handler 48 | app.use(function(err, req, res, next) { 49 | // set locals, only providing error in development 50 | res.locals.message = err.message; 51 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 52 | // render the error page 53 | res.status(err.status || 500); 54 | res.render('error'); 55 | }); 56 | 57 | module.exports = app; 58 | -------------------------------------------------------------------------------- /backend/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('campusride:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "campusride", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "cookie-parser": "~1.4.3", 10 | "debug": "~2.6.9", 11 | "express": "~4.16.0", 12 | "express-jwt": "^5.3.1", 13 | "http-errors": "~1.6.2", 14 | "jade": "~1.11.0", 15 | "jwks-rsa": "^1.2.1", 16 | "moment": "^2.22.0", 17 | "morgan": "~1.9.0", 18 | "mysql": "^2.15.0", 19 | "mysql2": "^1.5.3", 20 | "node-fetch": "^2.1.2", 21 | "string-format": "^1.0.0" 22 | }, 23 | "devDependencies": { 24 | "nodemon": "^1.17.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET home page. */ 5 | router.get('/', function(req, res, next) { 6 | res.render('index', { title: 'Express' }); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /backend/routes/public.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | const format = require('string-format'); 3 | format.extend(String.prototype, {}); 4 | var {query_send_data} = require('./utilities.js'); 5 | var router = express.Router(); 6 | var moment = require('moment'); 7 | 8 | let process_data; 9 | 10 | // Look up StopTimeTable 11 | process_data= (req, res, next)=>{ 12 | // auto timetable 13 | if(req.body.date&&req.body.pick_up_stop&&req.body.drop_off_stop){ 14 | let date='\'{}\''.format(req.body.date); 15 | let pick_up_stop=req.body.pick_up_stop; 16 | let drop_off_stop = req.body.drop_off_stop; 17 | /* version one 18 | let sql = 'select schedule_id, line_id, maker, capacity, year, pick_up_time, drop_off_time '+ 19 | ' from StopTimeTable, Schedule, Auto '+ 20 | ' where pick_up_stop='+pick_up_stop+ 21 | ' and drop_off_stop='+drop_off_stop+ 22 | ' and date='+date+ 23 | ' and Schedule.vehicle_id=StopTimeTable.vehicle_id '+ 24 | ' and Schedule.vehicle_id=Auto.vehicle_id'+ 25 | ' order by pick_up_time'; 26 | */ 27 | /* version two 28 | let sql = ' select Appointment.apt_id, Appointment.schedule_id, Appointment.pick_up_time, drop_off_time from '+ 29 | ' (select schedule_id, pick_up_time, pick_up_stop, drop_off_time, drop_off_stop '+ 30 | ' from StopTimeTable, Schedule '+ 31 | ' where pick_up_stop='+pick_up_stop+ 32 | ' and drop_off_stop='+drop_off_stop+ 33 | ' and date='+date+ 34 | ' and Schedule.vehicle_id=StopTimeTable.vehicle_id) '+ 35 | ' AutoTimeTable, Appointment, AutoRide'+ 36 | ' where Appointment.type=\'auto\' and Appointment.apt_id=AutoRide.apt_id'+ 37 | ' and Appointment.schedule_id=AutoTimeTable.schedule_id '+ 38 | ' and AutoTimeTable.pick_up_stop=Appointment.pick_up_stop and AutoRide.drop_off_stop=AutoTimeTable.drop_off_stop'; 39 | */ 40 | // version three: support seats_left query 41 | let AppointmentTable = '(select Appointment.apt_id, Appointment.schedule_id, stoptimetable.vehicle_id, stoptimetable.pick_up_time, drop_off_time '+ 42 | ' from Appointment, AutoRide, Schedule, stoptimetable '+ 43 | ' where '+ 44 | ' Appointment.type=\'auto\' and date='+date+ 45 | ' and Appointment.apt_id=AutoRide.apt_id '+ 46 | ' and Schedule.schedule_id=Appointment.schedule_id '+ 47 | ' and stoptimetable.vehicle_id = Schedule.vehicle_id '+ 48 | ' and stoptimetable.drop_off_stop = AutoRide.drop_off_stop '+ 49 | ' and stoptimetable.pick_up_stop = Appointment.pick_up_stop) AppointmentTable '; 50 | let SelectedStopTimeTable = '(select line_id, vehicle_id, pick_up_time, drop_off_time from stoptimetable '+ 51 | ' where pick_up_stop='+pick_up_stop+' and drop_off_stop='+drop_off_stop+') SelectedStopTimeTable'; 52 | let LeftJoinTable = '(select SelectedStopTimeTable.*, apt_id from '+ 53 | SelectedStopTimeTable + ' left join '+ AppointmentTable + 54 | ' on SelectedStopTimeTable.vehicle_id=AppointmentTable.vehicle_id '+ 55 | ' and not SelectedStopTimeTable.pick_up_time>AppointmentTable.drop_off_time '+ 56 | ' and not SelectedStopTimeTable.drop_off_timeapt_count '+ 62 | ' order by pick_up_time'; 63 | return query_send_data(FinalTable)(req, res, next); 64 | } 65 | // cat timetable 66 | else if(req.body.datetime){ 67 | const datetime = moment(req.body.datetime); 68 | let date = datetime.format('YYYY-MM-DD'); 69 | date = '\''+date+'\''; 70 | let time1 = datetime.subtract(0.5,'hours').format('hh:mm') 71 | time1 = '\'{}\''.format(time1); 72 | let time2 = datetime.add(1, 'hours').format('hh:mm'); 73 | time2 = '\'{}\''.format(time2); 74 | let sql = 75 | ' select schedule_id, Driver.driver_id, first_name as driver_first_name, last_name as driver_last_name, Cart.vehicle_id as cart_id, wheelchair_acce as accessibility from Schedule, Cart, Driver '+ 76 | ' where date={} '.format(date)+ 77 | ' and Cart.vehicle_id=Schedule.vehicle_id and Driver.driver_id=Schedule.driver_id '+ 78 | ' and not exists '+ 79 | ' (select * from Appointment where schedule_id=Schedule.schedule_id '+ 80 | ' and pick_up_time>{} and pick_up_time<{})'.format(time1, time2); 81 | return query_send_data(sql)(req, res, next); 82 | } 83 | console.log(req.body); 84 | res.status(400).send(JSON.stringify({error: 'illegal query!'})); 85 | } 86 | router.use('/timetable', process_data); 87 | 88 | // Look up StopInfo 89 | process_data = (req, res, next)=>{ 90 | let sql = 'select distinct stop_id, name from Stop order by stop_id'; 91 | return query_send_data(sql)(req, res, next); 92 | } 93 | router.use('/stops', process_data); 94 | 95 | 96 | 97 | module.exports = router; 98 | -------------------------------------------------------------------------------- /backend/routes/student.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | const {query_send_data, update_data}= require('./utilities.js') 4 | const format = require('string-format'); 5 | format.extend(String.prototype, {}); 6 | 7 | // student info 8 | process_data = (req, res, next)=>{ 9 | let sql='select email, first_name, last_name, cellphone, disabled_injured from Student where student_id='+req.user.student_id; 10 | return query_send_data(sql)(req, res, next) 11 | } 12 | router.use('/info/query', process_data); 13 | 14 | // update profile 15 | process_data = (req, res, next)=>{ 16 | if(req.body.first_name&&req.body.last_name&&('disabled_injured' in req.body)&&req.body.cellphone){ 17 | let sql='update Student set first_name=\'{first_name}\', last_name=\'{last_name}\', disabled_injured={disabled_injured}, cellphone=\'{cellphone}\' '.format(req.body); 18 | sql+=' where student_id='+req.user.student_id; 19 | return update_data(sql)(req, res, next); 20 | } 21 | next(); 22 | } 23 | router.use('/info/update', process_data); 24 | // make appointment 25 | process_data = (req, res, next)=>{ 26 | if(req.body.pick_up_stop&&req.body.pick_up_time) 27 | req.body.student_id = req.user.student_id; 28 | // auto ride 29 | if(req.body.drop_off_stop){ 30 | let sql = 'call insert_auto_ride('+ 31 | '{student_id}, {schedule_id}, \'{pick_up_time}\', {pick_up_stop}, {drop_off_stop})'.format(req.body); 32 | return update_data(sql)(req, res, next); 33 | }// private ride 34 | else{ 35 | let sql = 'insert into Appointment(student_id, schedule_id, type, pick_up_time, pick_up_stop)'+ 36 | ' values({student_id}, {schedule_id}, \'cart\', \'{pick_up_time}\', {pick_up_stop})'.format(req.body); 37 | return update_data(sql)(req, res, next); 38 | } 39 | next(); 40 | } 41 | router.use('/appointment/new', process_data); 42 | //query appontment 43 | process_data = (req, res, next)=>{ 44 | let sql = 'select Appointment.apt_id, date, pick_up_time, pick_up_stop, stop1.name as pick_up_stop_name, drop_off_stop, stop2.name as drop_off_stop_name '+ 45 | 'from Appointment, AutoRide, Schedule, Stop Stop1, Stop Stop2 '+ 46 | 'where student_id ='+req.user.student_id+' and type=\'auto\' and Appointment.apt_id = AutoRide.apt_id '+ 47 | 'and Schedule.schedule_id= Appointment.schedule_id and Stop1.stop_id=pick_up_stop and Stop2.stop_id=drop_off_stop and date>=curdate() '+ 48 | 'order by date, pick_up_time'; 49 | return query_send_data(sql)(req, res, next); 50 | } 51 | router.use('/appointment/query/auto', process_data); 52 | process_data = (req, res, next)=>{ 53 | let sql = 'select Appointment.apt_id, date, pick_up_time, pick_up_stop, Stop.name as pick_up_stop_name, first_name as driver_first_name, last_name as driver_last_name '+ 54 | 'from Appointment, Schedule, Driver, Stop '+ 55 | 'where student_id ='+req.user.student_id+' and type=\'cart\' '+ 56 | 'and Schedule.schedule_id= Appointment.schedule_id and Schedule.driver_id=Driver.driver_id and Stop.stop_id=pick_up_stop and date>=curdate() '+ 57 | 'order by date, pick_up_time'; 58 | return query_send_data(sql)(req, res, next); 59 | } 60 | router.use('/appointment/query/cart', process_data); 61 | //cancle appointment 62 | process_data = async (req, res, next)=>{ 63 | if(req.body.apt_ids){ 64 | const {findUserByAppointment} = require('../utils/user.js'); 65 | let student_id; 66 | let sql='delete from Appointment where '; 67 | let index = 0; 68 | for(let apt_id of req.body.apt_ids){ 69 | student_id = await findUserByAppointment(apt_id); 70 | if(student_id!=req.user.student_id){ 71 | res.status(401).send(); // unauthorized error 72 | return ; 73 | } 74 | if(index===0) sql+=' apt_id='+apt_id; 75 | else sql += ' or apt_id='+apt_id; 76 | index+=1; 77 | } 78 | if(index==0) return next(); 79 | return update_data(sql)(req, res, next); 80 | } 81 | next(); 82 | } 83 | router.use('/appointment/cancel', process_data); 84 | 85 | module.exports = router; 86 | -------------------------------------------------------------------------------- /backend/routes/utilities.js: -------------------------------------------------------------------------------- 1 | const query_send_data= (sql) => { 2 | return (req, res, next)=>{ 3 | // console.log(req.body); 4 | // console.log(sql); 5 | res.locals.connection.query( 6 | sql, (error, results, fields) =>{ 7 | if(error) {console.log(error); res.send(JSON.stringify({error: 'Opps, something went wrong. Please retry it.'}));} 8 | else 9 | res.send(JSON.stringify({fields: fields.map((f)=>f.name), 10 | data: results})); 11 | 12 | }); 13 | } 14 | } 15 | const update_data = (sql) =>{ 16 | return (req, res, next) =>{ 17 | // console.log(req.body); 18 | // console.log(sql); 19 | res.locals.connection.query( 20 | sql, (error, results, fields)=>{ 21 | if(error) {console.log(error); res.send(JSON.stringify({error: 'Opps, something went wrong. Please retry it.'}));} 22 | else res.send(JSON.stringify({message:'Success!'})); 23 | } 24 | ); 25 | } 26 | } 27 | module.exports = {query_send_data, update_data}; 28 | -------------------------------------------------------------------------------- /backend/utils/auth0-variables.js: -------------------------------------------------------------------------------- 1 | const domain = 'yourauth0-domain.auth0.com'; 2 | 3 | module.exports = {domain}; 4 | -------------------------------------------------------------------------------- /backend/utils/database-mysql.js: -------------------------------------------------------------------------------- 1 | const mysql = require('mysql'); 2 | const {MYSQL_CONFIG}=require('./database-variables.js') 3 | 4 | const mysqlConnection = mysql.createConnection({ 5 | host : MYSQL_CONFIG.host, 6 | user : MYSQL_CONFIG.user, 7 | password : MYSQL_CONFIG.password, 8 | database : MYSQL_CONFIG.database 9 | }); 10 | mysqlConnection.connect((err)=>{ 11 | if(err) throw err; 12 | console.log('DataBase: connect success ...'); 13 | }); 14 | 15 | module.exports = {mysqlConnection} 16 | -------------------------------------------------------------------------------- /backend/utils/database-mysql2.js: -------------------------------------------------------------------------------- 1 | // get the client 2 | // mysql2 enables use async connection and query 3 | const mysql = require('mysql2/promise'); 4 | const {MYSQL_CONFIG}=require('./database-variables.js') 5 | 6 | async function mysql2query(sql) { 7 | // create the connection 8 | const connection = await mysql.createConnection({ 9 | host : MYSQL_CONFIG.host, 10 | user : MYSQL_CONFIG.user, 11 | password : MYSQL_CONFIG.password, 12 | database : MYSQL_CONFIG.database}); 13 | // query database 14 | // console.log(sql); 15 | const [rows, fields] = await connection.execute(sql); 16 | return [rows, fields]; 17 | } 18 | 19 | module.exports = {mysql2query}; 20 | -------------------------------------------------------------------------------- /backend/utils/database-variables.js: -------------------------------------------------------------------------------- 1 | const MYSQL_CONFIG={ 2 | host : 'yourhosturl', 3 | user : 'yourusername', 4 | password : 'yourpassword', 5 | database : 'CampusRide' 6 | } 7 | module.exports = {MYSQL_CONFIG} 8 | -------------------------------------------------------------------------------- /backend/utils/jwtcheck.js: -------------------------------------------------------------------------------- 1 | var jwt = require('express-jwt'); 2 | var jwks = require('jwks-rsa'); 3 | const {domain} = require('./auth0-variables.js') 4 | var jwtCheck = jwt({ 5 | secret: jwks.expressJwtSecret({ 6 | cache: true, 7 | rateLimit: true, 8 | jwksRequestsPerMinute: 5, 9 | jwksUri: `https://${domain}/.well-known/jwks.json` 10 | }), 11 | audience: `https://${domain}/userinfo`, 12 | issuer: `https://${domain}/`, 13 | algorithms: ['RS256'] 14 | }); 15 | 16 | module.exports = jwtCheck; 17 | -------------------------------------------------------------------------------- /backend/utils/user.js: -------------------------------------------------------------------------------- 1 | var fetch = require("node-fetch"); 2 | const format = require('string-format'); 3 | format.extend(String.prototype, {}); 4 | var {mysql2query}=require('./database-mysql2.js') 5 | 6 | async function findUser(email){ 7 | let sql = 'select * from Student where email=\''+email+'\''; 8 | const [rows, fields] = await mysql2query(sql); 9 | if(rows.length === 0) 10 | return 0; 11 | return rows[0].student_id 12 | } 13 | async function createUser(profile){ 14 | let first_name = 'NULL'; 15 | let last_name = 'NULL'; 16 | let email = '\'{}\''.format(profile.email); 17 | // google profile use given_name and family_name 18 | if(profile.given_name) 19 | first_name = '\'{}\''.format(profile.given_name); 20 | if(profile.family_name) 21 | last_name = '\'{}\''.format(profile.family_name); 22 | const sql = 'insert into Student(email, first_name, last_name) values({},{},{})'.format(email, first_name, last_name); 23 | let result = await mysql2query(sql); 24 | // console.log(result); 25 | console.log('Create new user {}'.format(profile.email)); 26 | return await findUser(profile.email); 27 | } 28 | 29 | async function findOrCreateUser(req, res, next){ 30 | // get user profile from Auth0 31 | try{ 32 | const response = await fetch('https://qychen13.auth0.com/userinfo', {headers: {'authorization': req.headers.authorization}}); 33 | const profile = await response.json(); 34 | // console.log(profile); 35 | let student_id = await findUser(profile.email); 36 | if(student_id===0) 37 | student_id = await createUser(profile); 38 | req.user.student_id = student_id; 39 | req.body.student_id = student_id; // for the api legancy 40 | // console.log(req.user); 41 | next(); 42 | }catch(err){ 43 | console.log(err); 44 | return res.status(401).send(); 45 | } 46 | } 47 | 48 | async function findUserByAppointment(apt_id){ 49 | try{ 50 | const sql = 'select student_id from Appointment where apt_id='+apt_id; 51 | const [rows, fields] = await mysql2query(sql); 52 | if(rows.length ===0) 53 | return 0; // illegal apt_id found 54 | return rows[0].student_id; 55 | }catch(err){ 56 | console.log(err); 57 | return 0; //some error found error found 58 | } 59 | } 60 | 61 | module.exports = {findOrCreateUser, findUserByAppointment}; 62 | -------------------------------------------------------------------------------- /backend/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /backend/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /backend/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front_end", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "auth0-js": "^9.4.2", 7 | "moment": "^2.22.0", 8 | "nprogress": "^0.2.0", 9 | "r": "0.0.5", 10 | "react": "^16.3.1", 11 | "react-addons-css-transition-group": "^15.6.2", 12 | "react-datepicker": "^1.4.1", 13 | "react-datetime": "^2.14.0", 14 | "react-datetime-picker": "^1.3.1", 15 | "react-dom": "^16.3.1", 16 | "react-notifications": "^1.4.3", 17 | "react-router-dom": "^4.2.2", 18 | "react-scripts": "1.1.4" 19 | }, 20 | "scripts": { 21 | "start": "react-scripts start", 22 | "build": "react-scripts build", 23 | "test": "react-scripts test --env=jsdom", 24 | "eject": "react-scripts eject" 25 | }, 26 | "proxy": "http://localhost:3000" 27 | } 28 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Campus Ride 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 |
33 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /frontend/public/js/core/popper.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) Federico Zivolo 2017 3 | Distributed under the MIT License (license terms are at http://opensource.org/licenses/MIT). 4 | */(function(e,t){'object'==typeof exports&&'undefined'!=typeof module?module.exports=t():'function'==typeof define&&define.amd?define(t):e.Popper=t()})(this,function(){'use strict';function e(e){return e&&'[object Function]'==={}.toString.call(e)}function t(e,t){if(1!==e.nodeType)return[];var o=window.getComputedStyle(e,null);return t?o[t]:o}function o(e){return'HTML'===e.nodeName?e:e.parentNode||e.host}function n(e){if(!e||-1!==['HTML','BODY','#document'].indexOf(e.nodeName))return window.document.body;var i=t(e),r=i.overflow,p=i.overflowX,s=i.overflowY;return /(auto|scroll)/.test(r+s+p)?e:n(o(e))}function r(e){var o=e&&e.offsetParent,i=o&&o.nodeName;return i&&'BODY'!==i&&'HTML'!==i?-1!==['TD','TABLE'].indexOf(o.nodeName)&&'static'===t(o,'position')?r(o):o:window.document.documentElement}function p(e){var t=e.nodeName;return'BODY'!==t&&('HTML'===t||r(e.firstElementChild)===e)}function s(e){return null===e.parentNode?e:s(e.parentNode)}function d(e,t){if(!e||!e.nodeType||!t||!t.nodeType)return window.document.documentElement;var o=e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING,i=o?e:t,n=o?t:e,a=document.createRange();a.setStart(i,0),a.setEnd(n,0);var f=a.commonAncestorContainer;if(e!==f&&t!==f||i.contains(n))return p(f)?f:r(f);var l=s(e);return l.host?d(l.host,t):d(e,s(t).host)}function a(e){var t=1=o.clientWidth&&i>=o.clientHeight}),f=0i[e]&&!t.escapeWithReference&&(n=z(p[o],i[e]-('right'===e?p.width:p.height))),pe({},o,n)}};return n.forEach(function(e){var t=-1===['left','top'].indexOf(e)?'secondary':'primary';p=se({},p,s[t](e))}),e.offsets.popper=p,e},priority:['left','right','top','bottom'],padding:5,boundariesElement:'scrollParent'},keepTogether:{order:400,enabled:!0,fn:function(e){var t=e.offsets,o=t.popper,i=t.reference,n=e.placement.split('-')[0],r=V,p=-1!==['top','bottom'].indexOf(n),s=p?'right':'bottom',d=p?'left':'top',a=p?'width':'height';return o[s]r(i[s])&&(e.offsets.popper[d]=r(i[s])),e}},arrow:{order:500,enabled:!0,fn:function(e,t){if(!F(e.instance.modifiers,'arrow','keepTogether'))return e;var o=t.element;if('string'==typeof o){if(o=e.instance.popper.querySelector(o),!o)return e;}else if(!e.instance.popper.contains(o))return console.warn('WARNING: `arrow.element` must be child of its popper element!'),e;var i=e.placement.split('-')[0],n=e.offsets,r=n.popper,p=n.reference,s=-1!==['left','right'].indexOf(i),d=s?'height':'width',a=s?'top':'left',f=s?'left':'top',l=s?'bottom':'right',m=O(o)[d];p[l]-mr[l]&&(e.offsets.popper[a]+=p[a]+m-r[l]);var h=p[a]+p[d]/2-m/2,g=h-c(e.offsets.popper)[a];return g=_(z(r[d]-m,g),0),e.arrowElement=o,e.offsets.arrow={},e.offsets.arrow[a]=Math.round(g),e.offsets.arrow[f]='',e},element:'[x-arrow]'},flip:{order:600,enabled:!0,fn:function(e,t){if(W(e.instance.modifiers,'inner'))return e;if(e.flipped&&e.placement===e.originalPlacement)return e;var o=w(e.instance.popper,e.instance.reference,t.padding,t.boundariesElement),i=e.placement.split('-')[0],n=L(i),r=e.placement.split('-')[1]||'',p=[];switch(t.behavior){case fe.FLIP:p=[i,n];break;case fe.CLOCKWISE:p=K(i);break;case fe.COUNTERCLOCKWISE:p=K(i,!0);break;default:p=t.behavior;}return p.forEach(function(s,d){if(i!==s||p.length===d+1)return e;i=e.placement.split('-')[0],n=L(i);var a=e.offsets.popper,f=e.offsets.reference,l=V,m='left'===i&&l(a.right)>l(f.left)||'right'===i&&l(a.left)l(f.top)||'bottom'===i&&l(a.top)l(o.right),g=l(a.top)l(o.bottom),b='left'===i&&h||'right'===i&&c||'top'===i&&g||'bottom'===i&&u,y=-1!==['top','bottom'].indexOf(i),w=!!t.flipVariations&&(y&&'start'===r&&h||y&&'end'===r&&c||!y&&'start'===r&&g||!y&&'end'===r&&u);(m||b||w)&&(e.flipped=!0,(m||b)&&(i=p[d+1]),w&&(r=j(r)),e.placement=i+(r?'-'+r:''),e.offsets.popper=se({},e.offsets.popper,S(e.instance.popper,e.offsets.reference,e.placement)),e=N(e.instance.modifiers,e,'flip'))}),e},behavior:'flip',padding:5,boundariesElement:'viewport'},inner:{order:700,enabled:!1,fn:function(e){var t=e.placement,o=t.split('-')[0],i=e.offsets,n=i.popper,r=i.reference,p=-1!==['left','right'].indexOf(o),s=-1===['top','left'].indexOf(o);return n[p?'left':'top']=r[t]-(s?n[p?'width':'height']:0),e.placement=L(t),e.offsets.popper=c(n),e}},hide:{order:800,enabled:!0,fn:function(e){if(!F(e.instance.modifiers,'hide','preventOverflow'))return e;var t=e.offsets.reference,o=T(e.instance.modifiers,function(e){return'preventOverflow'===e.name}).boundaries;if(t.bottomo.right||t.top>o.bottom||t.right_/___.' >' "". 24 | * | | : `- \`.;`\ _ /`;.`/ - ` : | | 25 | * \ \ `_. \_ __\ /__ _/ .-` / / 26 | * =====`-.____`.___ \_____/___.-`___.-'===== 27 | * `=---=' 28 | * 29 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | * 31 | * Buddha Bless: "No Bugs" 32 | * 33 | * ========================================================= */ 34 | 35 | (function() { 36 | isWindows = navigator.platform.indexOf('Win') > -1 ? true : false; 37 | 38 | if (isWindows && !$('body').hasClass('sidebar-mini')) { 39 | // if we are on windows OS we activate the perfectScrollbar function 40 | $('.sidebar .sidebar-wrapper, .main-panel').perfectScrollbar(); 41 | 42 | $('html').addClass('perfect-scrollbar-on'); 43 | } else { 44 | $('html').addClass('perfect-scrollbar-off'); 45 | } 46 | })(); 47 | 48 | var breakCards = true; 49 | 50 | var searchVisible = 0; 51 | var transparent = true; 52 | 53 | var transparentDemo = true; 54 | var fixedTop = false; 55 | 56 | var mobile_menu_visible = 0, 57 | mobile_menu_initialized = false, 58 | toggle_initialized = false, 59 | bootstrap_nav_initialized = false; 60 | 61 | var seq = 0, 62 | delays = 80, 63 | durations = 500; 64 | var seq2 = 0, 65 | delays2 = 80, 66 | durations2 = 500; 67 | 68 | 69 | $(document).ready(function() { 70 | 71 | $('body').bootstrapMaterialDesign(); 72 | 73 | $sidebar = $('.sidebar'); 74 | 75 | md.initSidebarsCheck(); 76 | 77 | // if ($('body').hasClass('sidebar-mini')) { 78 | // md.misc.sidebar_mini_active = true; 79 | // } 80 | 81 | window_width = $(window).width(); 82 | 83 | // check if there is an image set for the sidebar's background 84 | md.checkSidebarImage(); 85 | 86 | md.initMinimizeSidebar(); 87 | 88 | // Activate bootstrap-select 89 | if ($(".selectpicker").length != 0) { 90 | $(".selectpicker").selectpicker(); 91 | } 92 | 93 | // Activate the tooltips 94 | $('[rel="tooltip"]').tooltip(); 95 | 96 | //Activate tags 97 | // we style the badges with our colors 98 | var tagClass = $('.tagsinput').data('color'); 99 | 100 | if($(".tagsinput").length != 0){ 101 | $('.tagsinput').tagsinput(); 102 | } 103 | 104 | $('.bootstrap-tagsinput').addClass(''+ tagClass +'-badge'); 105 | 106 | // Activate bootstrap-select 107 | $(".select").dropdown({ 108 | "dropdownClass": "dropdown-menu", 109 | "optionClass": "" 110 | }); 111 | 112 | $('.form-control').on("focus", function() { 113 | $(this).parent('.input-group').addClass("input-group-focus"); 114 | }).on("blur", function() { 115 | $(this).parent(".input-group").removeClass("input-group-focus"); 116 | }); 117 | 118 | 119 | if (breakCards == true) { 120 | // We break the cards headers if there is too much stress on them :-) 121 | $('[data-header-animation="true"]').each(function() { 122 | var $fix_button = $(this) 123 | var $card = $(this).parent('.card'); 124 | 125 | $card.find('.fix-broken-card').click(function() { 126 | console.log(this); 127 | var $header = $(this).parent().parent().siblings('.card-header, .card-header-image'); 128 | 129 | $header.removeClass('hinge').addClass('fadeInDown'); 130 | 131 | $card.attr('data-count', 0); 132 | 133 | setTimeout(function() { 134 | $header.removeClass('fadeInDown animate'); 135 | }, 480); 136 | }); 137 | 138 | $card.mouseenter(function() { 139 | var $this = $(this); 140 | hover_count = parseInt($this.attr('data-count'), 10) + 1 || 0; 141 | $this.attr("data-count", hover_count); 142 | 143 | if (hover_count >= 20) { 144 | $(this).children('.card-header, .card-header-image').addClass('hinge animated'); 145 | } 146 | }); 147 | }); 148 | } 149 | 150 | // remove class has-error for checkbox validation 151 | $('input[type="checkbox"][required="true"], input[type="radio"][required="true"]').on('click', function() { 152 | if ($(this).hasClass('error')) { 153 | $(this).closest('div').removeClass('has-error'); 154 | } 155 | }); 156 | 157 | }); 158 | 159 | $(document).on('click', '.navbar-toggler', function() { 160 | $toggle = $(this); 161 | 162 | if (mobile_menu_visible == 1) { 163 | $('html').removeClass('nav-open'); 164 | 165 | $('.close-layer').remove(); 166 | setTimeout(function() { 167 | $toggle.removeClass('toggled'); 168 | }, 400); 169 | 170 | mobile_menu_visible = 0; 171 | } else { 172 | setTimeout(function() { 173 | $toggle.addClass('toggled'); 174 | }, 430); 175 | 176 | var $layer = $('
'); 177 | 178 | if ($('body').find('.main-panel').length != 0) { 179 | $layer.appendTo(".main-panel"); 180 | 181 | } else if (($('body').hasClass('off-canvas-sidebar'))) { 182 | $layer.appendTo(".wrapper-full-page"); 183 | } 184 | 185 | setTimeout(function() { 186 | $layer.addClass('visible'); 187 | }, 100); 188 | 189 | $layer.click(function() { 190 | $('html').removeClass('nav-open'); 191 | mobile_menu_visible = 0; 192 | 193 | $layer.removeClass('visible'); 194 | 195 | setTimeout(function() { 196 | $layer.remove(); 197 | $toggle.removeClass('toggled'); 198 | 199 | }, 400); 200 | }); 201 | 202 | $('html').addClass('nav-open'); 203 | mobile_menu_visible = 1; 204 | 205 | } 206 | 207 | }); 208 | 209 | // activate collapse right menu when the windows is resized 210 | $(window).resize(function() { 211 | md.initSidebarsCheck(); 212 | 213 | // reset the seq for charts drawing animations 214 | seq = seq2 = 0; 215 | 216 | setTimeout(function() { 217 | demo.initDashboardPageCharts(); 218 | }, 500); 219 | }); 220 | 221 | md = { 222 | misc: { 223 | navbar_menu_visible: 0, 224 | active_collapse: true, 225 | disabled_collapse_init: 0, 226 | }, 227 | 228 | checkSidebarImage: function() { 229 | $sidebar = $('.sidebar'); 230 | image_src = $sidebar.data('image'); 231 | 232 | if (image_src !== undefined) { 233 | sidebar_container = '',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.5",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.5",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.5",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); -------------------------------------------------------------------------------- /frontend/public/js/plugins/material.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function o(t){return"undefined"==typeof t.which?!0:"number"==typeof t.which&&t.which>0?!t.ctrlKey&&!t.metaKey&&!t.altKey&&8!=t.which&&9!=t.which&&13!=t.which&&16!=t.which&&17!=t.which&&20!=t.which&&27!=t.which:!1}function i(o){var i=t(o);i.prop("disabled")||i.closest(".form-group").addClass("is-focused")}function n(o){o.closest("label").hover(function(){var o=t(this).find("input");o.prop("disabled")||i(o)},function(){e(t(this).find("input"))})}function e(o){t(o).closest(".form-group").removeClass("is-focused")}t.expr[":"].notmdproc=function(o){return t(o).data("mdproc")?!1:!0},t.material={options:{validate:!0,input:!0,ripples:!0,checkbox:!0,togglebutton:!0,radio:!0,arrive:!0,autofill:!1,withRipples:[".btn:not(.btn-link)",".card-image",".navbar a:not(.withoutripple)",".footer a:not(.withoutripple)",".dropdown-menu a",".nav-tabs a:not(.withoutripple)",".withripple",".pagination li:not(.active):not(.disabled) a:not(.withoutripple)"].join(","),inputElements:"input.form-control, textarea.form-control, select.form-control",checkboxElements:".checkbox > label > input[type=checkbox]",togglebuttonElements:".togglebutton > label > input[type=checkbox]",radioElements:".radio > label > input[type=radio]"},checkbox:function(o){var i=t(o?o:this.options.checkboxElements).filter(":notmdproc").data("mdproc",!0).after("");n(i)},togglebutton:function(o){var i=t(o?o:this.options.togglebuttonElements).filter(":notmdproc").data("mdproc",!0).after("");n(i)},radio:function(o){var i=t(o?o:this.options.radioElements).filter(":notmdproc").data("mdproc",!0).after("");n(i)},input:function(o){t(o?o:this.options.inputElements).filter(":notmdproc").data("mdproc",!0).each(function(){var o=t(this),i=o.closest(".form-group");0===i.length&&(o.wrap("
"),i=o.closest(".form-group")),o.attr("data-hint")&&(o.after("

"+o.attr("data-hint")+"

"),o.removeAttr("data-hint"));var n={"input-lg":"form-group-lg","input-sm":"form-group-sm"};if(t.each(n,function(t,n){o.hasClass(t)&&(o.removeClass(t),i.addClass(n))}),o.hasClass("floating-label")){var e=o.attr("placeholder");o.attr("placeholder",null).removeClass("floating-label");var a=o.attr("id"),r="";a&&(r="for='"+a+"'"),i.addClass("label-floating"),o.after("")}(null===o.val()||"undefined"==o.val()||""===o.val())&&i.addClass("is-empty"),i.append(""),i.find("input[type=file]").length>0&&i.addClass("is-fileinput")})},attachInputEventHandlers:function(){var n=this.options.validate;t(document).on("change",".checkbox input[type=checkbox]",function(){t(this).blur()}).on("keydown paste",".form-control",function(i){o(i)&&t(this).closest(".form-group").removeClass("is-empty")}).on("keyup change",".form-control",function(){var o=t(this),i=o.closest(".form-group"),e="undefined"==typeof o[0].checkValidity||o[0].checkValidity();""===o.val()?i.addClass("is-empty"):i.removeClass("is-empty"),n&&(e?i.removeClass("has-error"):i.addClass("has-error"))}).on("focus",".form-control, .form-group.is-fileinput",function(){i(this)}).on("blur",".form-control, .form-group.is-fileinput",function(){e(this)}).on("change",".form-group input",function(){var o=t(this);if("file"!=o.attr("type")){var i=o.closest(".form-group"),n=o.val();n?i.removeClass("is-empty"):i.addClass("is-empty")}}).on("change",".form-group.is-fileinput input[type='file']",function(){var o=t(this),i=o.closest(".form-group"),n="";t.each(this.files,function(t,o){n+=o.name+", "}),n=n.substring(0,n.length-2),n?i.removeClass("is-empty"):i.addClass("is-empty"),i.find("input.form-control[readonly]").val(n)})},ripples:function(o){t(o?o:this.options.withRipples).ripples()},autofill:function(){var o=setInterval(function(){t("input[type!=checkbox]").each(function(){var o=t(this);o.val()&&o.val()!==o.attr("value")&&o.trigger("change")})},100);setTimeout(function(){clearInterval(o)},1e4)},attachAutofillEventHandlers:function(){var o;t(document).on("focus","input",function(){var i=t(this).parents("form").find("input").not("[type=file]");o=setInterval(function(){i.each(function(){var o=t(this);o.val()!==o.attr("value")&&o.trigger("change")})},100)}).on("blur",".form-group input",function(){clearInterval(o)})},init:function(o){this.options=t.extend({},this.options,o);var i=t(document);t.fn.ripples&&this.options.ripples&&this.ripples(),this.options.input&&(this.input(),this.attachInputEventHandlers()),this.options.checkbox&&this.checkbox(),this.options.togglebutton&&this.togglebutton(),this.options.radio&&this.radio(),this.options.autofill&&(this.autofill(),this.attachAutofillEventHandlers()),document.arrive&&this.options.arrive&&(t.fn.ripples&&this.options.ripples&&i.arrive(this.options.withRipples,function(){t.material.ripples(t(this))}),this.options.input&&i.arrive(this.options.inputElements,function(){t.material.input(t(this))}),this.options.checkbox&&i.arrive(this.options.checkboxElements,function(){t.material.checkbox(t(this))}),this.options.radio&&i.arrive(this.options.radioElements,function(){t.material.radio(t(this))}),this.options.togglebutton&&i.arrive(this.options.togglebuttonElements,function(){t.material.togglebutton(t(this))}))}}}(jQuery),function(t,o,i,n){"use strict";function e(o,i){r=this,this.element=t(o),this.options=t.extend({},s,i),this._defaults=s,this._name=a,this.init()}var a="ripples",r=null,s={};e.prototype.init=function(){var i=this.element;i.on("mousedown touchstart",function(n){if(!r.isTouch()||"mousedown"!==n.type){i.find(".ripple-container").length||i.append('
');var e=i.children(".ripple-container"),a=r.getRelY(e,n),s=r.getRelX(e,n);if(a||s){var l=r.getRipplesColor(i),p=t("
");p.addClass("ripple").css({left:s,top:a,"background-color":l}),e.append(p),function(){return o.getComputedStyle(p[0]).opacity}(),r.rippleOn(i,p),setTimeout(function(){r.rippleEnd(p)},500),i.on("mouseup mouseleave touchend",function(){p.data("mousedown","off"),"off"===p.data("animating")&&r.rippleOut(p)})}}})},e.prototype.getNewSize=function(t,o){return Math.max(t.outerWidth(),t.outerHeight())/o.outerWidth()*2.5},e.prototype.getRelX=function(t,o){var i=t.offset();return r.isTouch()?(o=o.originalEvent,1===o.touches.length?o.touches[0].pageX-i.left:!1):o.pageX-i.left},e.prototype.getRelY=function(t,o){var i=t.offset();return r.isTouch()?(o=o.originalEvent,1===o.touches.length?o.touches[0].pageY-i.top:!1):o.pageY-i.top},e.prototype.getRipplesColor=function(t){var i=t.data("ripple-color")?t.data("ripple-color"):o.getComputedStyle(t[0]).color;return i},e.prototype.hasTransitionSupport=function(){var t=i.body||i.documentElement,o=t.style,e=o.transition!==n||o.WebkitTransition!==n||o.MozTransition!==n||o.MsTransition!==n||o.OTransition!==n;return e},e.prototype.isTouch=function(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)},e.prototype.rippleEnd=function(t){t.data("animating","off"),"off"===t.data("mousedown")&&r.rippleOut(t)},e.prototype.rippleOut=function(t){t.off(),r.hasTransitionSupport()?t.addClass("ripple-out"):t.animate({opacity:0},100,function(){t.trigger("transitionend")}),t.on("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd",function(){t.remove()})},e.prototype.rippleOn=function(t,o){var i=r.getNewSize(t,o);r.hasTransitionSupport()?o.css({"-ms-transform":"scale("+i+")","-moz-transform":"scale("+i+")","-webkit-transform":"scale("+i+")",transform:"scale("+i+")"}).addClass("ripple-on").data("animating","on").data("mousedown","on"):o.animate({width:2*Math.max(t.outerWidth(),t.outerHeight()),height:2*Math.max(t.outerWidth(),t.outerHeight()),"margin-left":-1*Math.max(t.outerWidth(),t.outerHeight()),"margin-top":-1*Math.max(t.outerWidth(),t.outerHeight()),opacity:.2},500,function(){o.trigger("transitionend")})},t.fn.ripples=function(o){return this.each(function(){t.data(this,"plugin_"+a)||t.data(this,"plugin_"+a,new e(this,o))})}}(jQuery,window,document); 2 | -------------------------------------------------------------------------------- /frontend/public/js/plugins/perfect-scrollbar.jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * perfect-scrollbar v1.3.0 3 | * (c) 2017 Hyunje Jun 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.PerfectScrollbar=e()}(this,function(){"use strict";function t(t){return getComputedStyle(t)}function e(t,e){for(var i in e){var r=e[i];"number"==typeof r&&(r+="px"),t.style[i]=r}return t}function i(t){var e=document.createElement("div");return e.className=t,e}function r(t,e){if(!v)throw new Error("No element matching method supported");return v.call(t,e)}function l(t){t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)}function n(t,e){return Array.prototype.filter.call(t.children,function(t){return r(t,e)})}function o(t,e){var i=t.element.classList,r=m.state.scrolling(e);i.contains(r)?clearTimeout(Y[e]):i.add(r)}function s(t,e){Y[e]=setTimeout(function(){return t.isAlive&&t.element.classList.remove(m.state.scrolling(e))},t.settings.scrollingThreshold)}function a(t,e){o(t,e),s(t,e)}function c(t){if("function"==typeof window.CustomEvent)return new CustomEvent(t);var e=document.createEvent("CustomEvent");return e.initCustomEvent(t,!1,!1,void 0),e}function h(t,e,i,r,l){var n=i[0],o=i[1],s=i[2],h=i[3],u=i[4],d=i[5];void 0===r&&(r=!0),void 0===l&&(l=!1);var f=t.element;t.reach[h]=null,f[s]<1&&(t.reach[h]="start"),f[s]>t[n]-t[o]-1&&(t.reach[h]="end"),e&&(f.dispatchEvent(c("ps-scroll-"+h)),e<0?f.dispatchEvent(c("ps-scroll-"+u)):e>0&&f.dispatchEvent(c("ps-scroll-"+d)),r&&a(t,h)),t.reach[h]&&(e||l)&&f.dispatchEvent(c("ps-"+h+"-reach-"+t.reach[h]))}function u(t){return parseInt(t,10)||0}function d(t){return r(t,"input,[contenteditable]")||r(t,"select,[contenteditable]")||r(t,"textarea,[contenteditable]")||r(t,"button,[contenteditable]")}function f(e){var i=t(e);return u(i.width)+u(i.paddingLeft)+u(i.paddingRight)+u(i.borderLeftWidth)+u(i.borderRightWidth)}function p(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function b(t,i){var r={width:i.railXWidth};i.isRtl?r.left=i.negativeScrollAdjustment+t.scrollLeft+i.containerWidth-i.contentWidth:r.left=t.scrollLeft,i.isScrollbarXUsingBottom?r.bottom=i.scrollbarXBottom-t.scrollTop:r.top=i.scrollbarXTop+t.scrollTop,e(i.scrollbarXRail,r);var l={top:t.scrollTop,height:i.railYHeight};i.isScrollbarYUsingRight?i.isRtl?l.right=i.contentWidth-(i.negativeScrollAdjustment+t.scrollLeft)-i.scrollbarYRight-i.scrollbarYOuterWidth:l.right=i.scrollbarYRight-t.scrollLeft:i.isRtl?l.left=i.negativeScrollAdjustment+t.scrollLeft+2*i.containerWidth-i.contentWidth-i.scrollbarYLeft-i.scrollbarYOuterWidth:l.left=i.scrollbarYLeft+t.scrollLeft,e(i.scrollbarYRail,l),e(i.scrollbarX,{left:i.scrollbarXLeft,width:i.scrollbarXWidth-i.railBorderXWidth}),e(i.scrollbarY,{top:i.scrollbarYTop,height:i.scrollbarYHeight-i.railBorderYWidth})}function g(t,e){function i(e){p[d]=b+v*(e[a]-g),o(t,f),T(t),e.stopPropagation(),e.preventDefault()}function r(){s(t,f),t.event.unbind(t.ownerDocument,"mousemove",i)}var l=e[0],n=e[1],a=e[2],c=e[3],h=e[4],u=e[5],d=e[6],f=e[7],p=t.element,b=null,g=null,v=null;t.event.bind(t[h],"mousedown",function(e){b=p[d],g=e[a],v=(t[n]-t[l])/(t[c]-t[u]),t.event.bind(t.ownerDocument,"mousemove",i),t.event.once(t.ownerDocument,"mouseup",r),e.stopPropagation(),e.preventDefault()})}var v="undefined"!=typeof Element&&(Element.prototype.matches||Element.prototype.webkitMatchesSelector||Element.prototype.msMatchesSelector),m={main:"ps",element:{thumb:function(t){return"ps__thumb-"+t},rail:function(t){return"ps__rail-"+t},consuming:"ps__child--consume"},state:{focus:"ps--focus",active:function(t){return"ps--active-"+t},scrolling:function(t){return"ps--scrolling-"+t}}},Y={x:null,y:null},X=function(t){this.element=t,this.handlers={}},w={isEmpty:{configurable:!0}};X.prototype.bind=function(t,e){void 0===this.handlers[t]&&(this.handlers[t]=[]),this.handlers[t].push(e),this.element.addEventListener(t,e,!1)},X.prototype.unbind=function(t,e){var i=this;this.handlers[t]=this.handlers[t].filter(function(r){return!(!e||r===e)||(i.element.removeEventListener(t,r,!1),!1)})},X.prototype.unbindAll=function(){var t=this;for(var e in t.handlers)t.unbind(e)},w.isEmpty.get=function(){var t=this;return Object.keys(this.handlers).every(function(e){return 0===t.handlers[e].length})},Object.defineProperties(X.prototype,w);var y=function(){this.eventElements=[]};y.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return e||(e=new X(t),this.eventElements.push(e)),e},y.prototype.bind=function(t,e,i){this.eventElement(t).bind(e,i)},y.prototype.unbind=function(t,e,i){var r=this.eventElement(t);r.unbind(e,i),r.isEmpty&&this.eventElements.splice(this.eventElements.indexOf(r),1)},y.prototype.unbindAll=function(){this.eventElements.forEach(function(t){return t.unbindAll()}),this.eventElements=[]},y.prototype.once=function(t,e,i){var r=this.eventElement(t),l=function(t){r.unbind(e,l),i(t)};r.bind(e,l)};var W=function(t,e,i,r,l){void 0===r&&(r=!0),void 0===l&&(l=!1);var n;if("top"===e)n=["contentHeight","containerHeight","scrollTop","y","up","down"];else{if("left"!==e)throw new Error("A proper axis should be provided");n=["contentWidth","containerWidth","scrollLeft","x","left","right"]}h(t,i,n,r,l)},L={isWebKit:"undefined"!=typeof document&&"WebkitAppearance"in document.documentElement.style,supportsTouch:"undefined"!=typeof window&&("ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch),supportsIePointer:"undefined"!=typeof navigator&&navigator.msMaxTouchPoints,isChrome:"undefined"!=typeof navigator&&/Chrome/i.test(navigator&&navigator.userAgent)},T=function(t){var e=t.element;t.containerWidth=e.clientWidth,t.containerHeight=e.clientHeight,t.contentWidth=e.scrollWidth,t.contentHeight=e.scrollHeight,e.contains(t.scrollbarXRail)||(n(e,m.element.rail("x")).forEach(function(t){return l(t)}),e.appendChild(t.scrollbarXRail)),e.contains(t.scrollbarYRail)||(n(e,m.element.rail("y")).forEach(function(t){return l(t)}),e.appendChild(t.scrollbarYRail)),!t.settings.suppressScrollX&&t.containerWidth+t.settings.scrollXMarginOffset=t.railXWidth-t.scrollbarXWidth&&(t.scrollbarXLeft=t.railXWidth-t.scrollbarXWidth),t.scrollbarYTop>=t.railYHeight-t.scrollbarYHeight&&(t.scrollbarYTop=t.railYHeight-t.scrollbarYHeight),b(e,t),t.scrollbarXActive?e.classList.add(m.state.active("x")):(e.classList.remove(m.state.active("x")),t.scrollbarXWidth=0,t.scrollbarXLeft=0,e.scrollLeft=0),t.scrollbarYActive?e.classList.add(m.state.active("y")):(e.classList.remove(m.state.active("y")),t.scrollbarYHeight=0,t.scrollbarYTop=0,e.scrollTop=0)},R={"click-rail":function(t){t.event.bind(t.scrollbarY,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarYRail,"mousedown",function(e){var i=e.pageY-window.pageYOffset-t.scrollbarYRail.getBoundingClientRect().top>t.scrollbarYTop?1:-1;t.element.scrollTop+=i*t.containerHeight,T(t),e.stopPropagation()}),t.event.bind(t.scrollbarX,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarXRail,"mousedown",function(e){var i=e.pageX-window.pageXOffset-t.scrollbarXRail.getBoundingClientRect().left>t.scrollbarXLeft?1:-1;t.element.scrollLeft+=i*t.containerWidth,T(t),e.stopPropagation()})},"drag-thumb":function(t){g(t,["containerWidth","contentWidth","pageX","railXWidth","scrollbarX","scrollbarXWidth","scrollLeft","x"]),g(t,["containerHeight","contentHeight","pageY","railYHeight","scrollbarY","scrollbarYHeight","scrollTop","y"])},keyboard:function(t){function e(e,r){var l=i.scrollTop;if(0===e){if(!t.scrollbarYActive)return!1;if(0===l&&r>0||l>=t.contentHeight-t.containerHeight&&r<0)return!t.settings.wheelPropagation}var n=i.scrollLeft;if(0===r){if(!t.scrollbarXActive)return!1;if(0===n&&e<0||n>=t.contentWidth-t.containerWidth&&e>0)return!t.settings.wheelPropagation}return!0}var i=t.element,l=function(){return r(i,":hover")},n=function(){return r(t.scrollbarX,":focus")||r(t.scrollbarY,":focus")};t.event.bind(t.ownerDocument,"keydown",function(r){if(!(r.isDefaultPrevented&&r.isDefaultPrevented()||r.defaultPrevented)&&(l()||n())){var o=document.activeElement?document.activeElement:t.ownerDocument.activeElement;if(o){if("IFRAME"===o.tagName)o=o.contentDocument.activeElement;else for(;o.shadowRoot;)o=o.shadowRoot.activeElement;if(d(o))return}var s=0,a=0;switch(r.which){case 37:s=r.metaKey?-t.contentWidth:r.altKey?-t.containerWidth:-30;break;case 38:a=r.metaKey?t.contentHeight:r.altKey?t.containerHeight:30;break;case 39:s=r.metaKey?t.contentWidth:r.altKey?t.containerWidth:30;break;case 40:a=r.metaKey?-t.contentHeight:r.altKey?-t.containerHeight:-30;break;case 32:a=r.shiftKey?t.containerHeight:-t.containerHeight;break;case 33:a=t.containerHeight;break;case 34:a=-t.containerHeight;break;case 36:a=t.contentHeight;break;case 35:a=-t.contentHeight;break;default:return}t.settings.suppressScrollX&&0!==s||t.settings.suppressScrollY&&0!==a||(i.scrollTop-=a,i.scrollLeft+=s,T(t),e(s,a)&&r.preventDefault())}})},wheel:function(e){function i(t,i){var r=0===o.scrollTop,l=o.scrollTop+o.offsetHeight===o.scrollHeight,n=0===o.scrollLeft,s=o.scrollLeft+o.offsetWidth===o.offsetWidth;return!(Math.abs(i)>Math.abs(t)?r||l:n||s)||!e.settings.wheelPropagation}function r(t){var e=t.deltaX,i=-1*t.deltaY;return void 0!==e&&void 0!==i||(e=-1*t.wheelDeltaX/6,i=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,i*=10),e!==e&&i!==i&&(e=0,i=t.wheelDelta),t.shiftKey?[-i,-e]:[e,i]}function l(e,i,r){if(!L.isWebKit&&o.querySelector("select:focus"))return!0;if(!o.contains(e))return!1;for(var l=e;l&&l!==o;){if(l.classList.contains(m.element.consuming))return!0;var n=t(l);if([n.overflow,n.overflowX,n.overflowY].join("").match(/(scroll|auto)/)){var s=l.scrollHeight-l.clientHeight;if(s>0&&!(0===l.scrollTop&&r>0||l.scrollTop===s&&r<0))return!0;var a=l.scrollLeft-l.clientWidth;if(a>0&&!(0===l.scrollLeft&&i<0||l.scrollLeft===a&&i>0))return!0}l=l.parentNode}return!1}function n(t){var n=r(t),s=n[0],a=n[1];if(!l(t.target,s,a)){var c=!1;e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(a?o.scrollTop-=a*e.settings.wheelSpeed:o.scrollTop+=s*e.settings.wheelSpeed,c=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(s?o.scrollLeft+=s*e.settings.wheelSpeed:o.scrollLeft-=a*e.settings.wheelSpeed,c=!0):(o.scrollTop-=a*e.settings.wheelSpeed,o.scrollLeft+=s*e.settings.wheelSpeed),T(e),(c=c||i(s,a))&&!t.ctrlKey&&(t.stopPropagation(),t.preventDefault())}}var o=e.element;void 0!==window.onwheel?e.event.bind(o,"wheel",n):void 0!==window.onmousewheel&&e.event.bind(o,"mousewheel",n)},touch:function(e){function i(t,i){var r=h.scrollTop,l=h.scrollLeft,n=Math.abs(t),o=Math.abs(i);if(o>n){if(i<0&&r===e.contentHeight-e.containerHeight||i>0&&0===r)return 0===window.scrollY&&i>0&&L.isChrome}else if(n>o&&(t<0&&l===e.contentWidth-e.containerWidth||t>0&&0===l))return!0;return!0}function r(t,i){h.scrollTop-=i,h.scrollLeft-=t,T(e)}function l(t){return t.targetTouches?t.targetTouches[0]:t}function n(t){return!(t.pointerType&&"pen"===t.pointerType&&0===t.buttons||(!t.targetTouches||1!==t.targetTouches.length)&&(!t.pointerType||"mouse"===t.pointerType||t.pointerType===t.MSPOINTER_TYPE_MOUSE))}function o(t){if(n(t)){var e=l(t);u.pageX=e.pageX,u.pageY=e.pageY,d=(new Date).getTime(),null!==p&&clearInterval(p)}}function s(e,i,r){if(!h.contains(e))return!1;for(var l=e;l&&l!==h;){if(l.classList.contains(m.element.consuming))return!0;var n=t(l);if([n.overflow,n.overflowX,n.overflowY].join("").match(/(scroll|auto)/)){var o=l.scrollHeight-l.clientHeight;if(o>0&&!(0===l.scrollTop&&r>0||l.scrollTop===o&&r<0))return!0;var s=l.scrollLeft-l.clientWidth;if(s>0&&!(0===l.scrollLeft&&i<0||l.scrollLeft===s&&i>0))return!0}l=l.parentNode}return!1}function a(t){if(n(t)){var e=l(t),o={pageX:e.pageX,pageY:e.pageY},a=o.pageX-u.pageX,c=o.pageY-u.pageY;if(s(t.target,a,c))return;r(a,c),u=o;var h=(new Date).getTime(),p=h-d;p>0&&(f.x=a/p,f.y=c/p,d=h),i(a,c)&&t.preventDefault()}}function c(){e.settings.swipeEasing&&(clearInterval(p),p=setInterval(function(){e.isInitialized?clearInterval(p):f.x||f.y?Math.abs(f.x)<.01&&Math.abs(f.y)<.01?clearInterval(p):(r(30*f.x,30*f.y),f.x*=.8,f.y*=.8):clearInterval(p)},10))}if(L.supportsTouch||L.supportsIePointer){var h=e.element,u={},d=0,f={},p=null;L.supportsTouch?(e.event.bind(h,"touchstart",o),e.event.bind(h,"touchmove",a),e.event.bind(h,"touchend",c)):L.supportsIePointer&&(window.PointerEvent?(e.event.bind(h,"pointerdown",o),e.event.bind(h,"pointermove",a),e.event.bind(h,"pointerup",c)):window.MSPointerEvent&&(e.event.bind(h,"MSPointerDown",o),e.event.bind(h,"MSPointerMove",a),e.event.bind(h,"MSPointerUp",c)))}}},H=function(r,l){var n=this;if(void 0===l&&(l={}),"string"==typeof r&&(r=document.querySelector(r)),!r||!r.nodeName)throw new Error("no element is specified to initialize PerfectScrollbar");this.element=r,r.classList.add(m.main),this.settings={handlers:["click-rail","drag-thumb","keyboard","wheel","touch"],maxScrollbarLength:null,minScrollbarLength:null,scrollingThreshold:1e3,scrollXMarginOffset:0,scrollYMarginOffset:0,suppressScrollX:!1,suppressScrollY:!1,swipeEasing:!0,useBothWheelAxes:!1,wheelPropagation:!1,wheelSpeed:1};for(var o in l)n.settings[o]=l[o];this.containerWidth=null,this.containerHeight=null,this.contentWidth=null,this.contentHeight=null;var s=function(){return r.classList.add(m.state.focus)},a=function(){return r.classList.remove(m.state.focus)};this.isRtl="rtl"===t(r).direction,this.isNegativeScroll=function(){var t=r.scrollLeft,e=null;return r.scrollLeft=-1,e=r.scrollLeft<0,r.scrollLeft=t,e}(),this.negativeScrollAdjustment=this.isNegativeScroll?r.scrollWidth-r.clientWidth:0,this.event=new y,this.ownerDocument=r.ownerDocument||document,this.scrollbarXRail=i(m.element.rail("x")),r.appendChild(this.scrollbarXRail),this.scrollbarX=i(m.element.thumb("x")),this.scrollbarXRail.appendChild(this.scrollbarX),this.scrollbarX.setAttribute("tabindex",0),this.event.bind(this.scrollbarX,"focus",s),this.event.bind(this.scrollbarX,"blur",a),this.scrollbarXActive=null,this.scrollbarXWidth=null,this.scrollbarXLeft=null;var c=t(this.scrollbarXRail);this.scrollbarXBottom=parseInt(c.bottom,10),isNaN(this.scrollbarXBottom)?(this.isScrollbarXUsingBottom=!1,this.scrollbarXTop=u(c.top)):this.isScrollbarXUsingBottom=!0,this.railBorderXWidth=u(c.borderLeftWidth)+u(c.borderRightWidth),e(this.scrollbarXRail,{display:"block"}),this.railXMarginWidth=u(c.marginLeft)+u(c.marginRight),e(this.scrollbarXRail,{display:""}),this.railXWidth=null,this.railXRatio=null,this.scrollbarYRail=i(m.element.rail("y")),r.appendChild(this.scrollbarYRail),this.scrollbarY=i(m.element.thumb("y")),this.scrollbarYRail.appendChild(this.scrollbarY),this.scrollbarY.setAttribute("tabindex",0),this.event.bind(this.scrollbarY,"focus",s),this.event.bind(this.scrollbarY,"blur",a),this.scrollbarYActive=null,this.scrollbarYHeight=null,this.scrollbarYTop=null;var h=t(this.scrollbarYRail);this.scrollbarYRight=parseInt(h.right,10),isNaN(this.scrollbarYRight)?(this.isScrollbarYUsingRight=!1,this.scrollbarYLeft=u(h.left)):this.isScrollbarYUsingRight=!0,this.scrollbarYOuterWidth=this.isRtl?f(this.scrollbarY):null,this.railBorderYWidth=u(h.borderTopWidth)+u(h.borderBottomWidth),e(this.scrollbarYRail,{display:"block"}),this.railYMarginHeight=u(h.marginTop)+u(h.marginBottom),e(this.scrollbarYRail,{display:""}),this.railYHeight=null,this.railYRatio=null,this.reach={x:r.scrollLeft<=0?"start":r.scrollLeft>=this.contentWidth-this.containerWidth?"end":null,y:r.scrollTop<=0?"start":r.scrollTop>=this.contentHeight-this.containerHeight?"end":null},this.isAlive=!0,this.settings.handlers.forEach(function(t){return R[t](n)}),this.lastScrollTop=r.scrollTop,this.lastScrollLeft=r.scrollLeft,this.event.bind(this.element,"scroll",function(t){return n.onScroll(t)}),T(this)};return H.prototype.update=function(){this.isAlive&&(this.negativeScrollAdjustment=this.isNegativeScroll?this.element.scrollWidth-this.element.clientWidth:0,e(this.scrollbarXRail,{display:"block"}),e(this.scrollbarYRail,{display:"block"}),this.railXMarginWidth=u(t(this.scrollbarXRail).marginLeft)+u(t(this.scrollbarXRail).marginRight),this.railYMarginHeight=u(t(this.scrollbarYRail).marginTop)+u(t(this.scrollbarYRail).marginBottom),e(this.scrollbarXRail,{display:"none"}),e(this.scrollbarYRail,{display:"none"}),T(this),W(this,"top",0,!1,!0),W(this,"left",0,!1,!0),e(this.scrollbarXRail,{display:""}),e(this.scrollbarYRail,{display:""}))},H.prototype.onScroll=function(t){this.isAlive&&(T(this),W(this,"top",this.element.scrollTop-this.lastScrollTop),W(this,"left",this.element.scrollLeft-this.lastScrollLeft),this.lastScrollTop=this.element.scrollTop,this.lastScrollLeft=this.element.scrollLeft)},H.prototype.destroy=function(){this.isAlive&&(this.event.unbindAll(),l(this.scrollbarX),l(this.scrollbarY),l(this.scrollbarXRail),l(this.scrollbarYRail),this.removePsClasses(),this.element=null,this.scrollbarX=null,this.scrollbarY=null,this.scrollbarXRail=null,this.scrollbarYRail=null,this.isAlive=!1)},H.prototype.removePsClasses=function(){this.element.className=this.element.className.split(" ").filter(function(t){return!t.match(/^ps([-_].+|)$/)}).join(" ")},H}); 7 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/Auth/Auth.js: -------------------------------------------------------------------------------- 1 | // reference: https://auth0.com/docs/quickstart/spa/react/01-login 2 | import auth0 from 'auth0-js'; 3 | import { AUTH_CONFIG } from './auth0-variables'; 4 | import history from '../history'; 5 | 6 | export default class Auth { 7 | auth0 = new auth0.WebAuth({ 8 | domain: AUTH_CONFIG.domain, 9 | clientID: AUTH_CONFIG.clientId, 10 | redirectUri: AUTH_CONFIG.callbackUrl, 11 | audience: AUTH_CONFIG.audience, 12 | responseType: 'token id_token', 13 | scope: 'openid profile email' 14 | }); 15 | 16 | constructor() { 17 | this.login = this.login.bind(this); 18 | this.logout = this.logout.bind(this); 19 | this.handleAuthentication = this.handleAuthentication.bind(this); 20 | this.isAuthenticated = this.isAuthenticated.bind(this); 21 | this.scheduleRenewal(); 22 | this.requireAuth = this.requireAuth.bind(this); 23 | } 24 | 25 | login() { 26 | this.auth0.authorize(); 27 | } 28 | 29 | handleAuthentication() { 30 | this.auth0.parseHash((err, authResult) => { 31 | if (authResult && authResult.accessToken && authResult.idToken) { 32 | this.setSession(authResult); 33 | history.replace('/') 34 | window.location.reload(); 35 | } else if (err) { 36 | console.log(err); 37 | alert(`Error: ${err.error}.`); 38 | } 39 | }); 40 | } 41 | 42 | setSession(authResult) { 43 | // Set the time that the access token will expire at 44 | let expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime()); 45 | localStorage.setItem('access_token', authResult.accessToken); 46 | localStorage.setItem('id_token', authResult.idToken); 47 | localStorage.setItem('expires_at', expiresAt); 48 | // schedule a token renewal 49 | this.scheduleRenewal(); 50 | } 51 | 52 | logout() { 53 | // Clear access token and ID token from local storage 54 | localStorage.removeItem('access_token'); 55 | localStorage.removeItem('id_token'); 56 | localStorage.removeItem('expires_at'); 57 | // navigate to the home route 58 | clearTimeout(this.tokenRenewalTimeout); 59 | // navigate to the home route 60 | history.replace('/'); 61 | window.location.reload(); 62 | 63 | } 64 | 65 | isAuthenticated() { 66 | // Check whether the current time is past the 67 | // access token's expiry time 68 | let expiresAt = JSON.parse(localStorage.getItem('expires_at')); 69 | return new Date().getTime() < expiresAt; 70 | } 71 | renewToken() { 72 | this.auth0.checkSession({}, 73 | (err, result) => { 74 | if (err) { 75 | alert( 76 | `Could not get a new token (${err.error}: ${err.error_description}).` 77 | ); 78 | history.replace('/'); 79 | window.location.reload(); 80 | } else { 81 | this.setSession(result); 82 | console.log(`Successfully renewed auth!`); 83 | } 84 | } 85 | ); 86 | } 87 | 88 | scheduleRenewal() { 89 | const expiresAt = JSON.parse(localStorage.getItem('expires_at')); 90 | const delay = expiresAt - Date.now(); 91 | if (delay > 0) { 92 | this.tokenRenewalTimeout = setTimeout(() => { 93 | this.renewToken(); 94 | }, delay); 95 | } 96 | } 97 | 98 | requireAuth(nextState, replace){ 99 | if(!this.isAuthenticated) replace({pathname:'/'}); 100 | } 101 | } 102 | 103 | export function getAccessToken(){ 104 | const accessToken = localStorage.getItem('access_token'); 105 | if (!accessToken) { 106 | throw new Error('No access token found'); 107 | } 108 | return accessToken; 109 | } 110 | -------------------------------------------------------------------------------- /frontend/src/Auth/auth0-variables.js: -------------------------------------------------------------------------------- 1 | export const AUTH_CONFIG = { 2 | domain: 'yourauth0-domain.auth0.com', 3 | clientId: 'your-auth0-clientId', 4 | callbackUrl: 'http://localhost:3001/callback', // you need set call back url at Auth0 5 | audience: 'your-audience' 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/api/public.js: -------------------------------------------------------------------------------- 1 | import sendRequest from './sendRequest'; 2 | 3 | export const fetchStopInfo=()=> sendRequest('/public/stops'); 4 | export const fetchTimeTable=(body)=> sendRequest('/public/timetable', body); 5 | -------------------------------------------------------------------------------- /frontend/src/api/sendRequest.js: -------------------------------------------------------------------------------- 1 | import {getAccessToken} from '../Auth/Auth.js'; 2 | import Notification from '../components/Notification.js'; 3 | 4 | export default async function sendRequest(path, body = null, token=false) { 5 | let req={'headers':{}}; 6 | if(token){ 7 | req.headers.Authorization = 'Bearer '+getAccessToken(); 8 | } 9 | if(body){ 10 | req.method='POST' 11 | req.headers['Content-Type']='application/json'; 12 | req.body = JSON.stringify(body); 13 | // console.log(body); 14 | } 15 | try{ 16 | if(req.headers === {}) req = null; 17 | const response = await fetch(path, req); 18 | if(response.status>=400) { 19 | if(response.status==401) 20 | Notification.showError('You must login to do that!'); 21 | else 22 | Notification.showError('Bad response from server: status'+response.status); 23 | return {fields:[], data:[]}; 24 | }; 25 | const data = await response.json(); 26 | if(data.error) {Notification.showError(data.error); return {fields:[], data:[]};} 27 | if(data.message) Notification.showSuccess(data.message); 28 | return data; 29 | }catch(err){ 30 | console.log(err); 31 | throw err; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/api/student.js: -------------------------------------------------------------------------------- 1 | import sendRequest from './sendRequest'; 2 | 3 | export const getStudentInfo=()=>sendRequest('/student/info/query', null, true); 4 | export const updateStudentInfo=(profile)=>sendRequest('/student/info/update', profile, true); 5 | export const makeAppointment=(schedule)=>sendRequest('/student/appointment/new', schedule, true); 6 | export const getAutoAppointment=()=>sendRequest('/student/appointment/query/auto', null, true); 7 | export const getCartAppointment=()=>sendRequest('/student/appointment/query/cart', null, true); 8 | export const cancelAppointment=(apt_ids)=>sendRequest('/student/appointment/cancel', {apt_ids: apt_ids}, true); 9 | -------------------------------------------------------------------------------- /frontend/src/components/Callback.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Callback extends Component { 4 | 5 | componentDidMount() { 6 | this.props.auth.handleAuthentication(); 7 | 8 | } 9 | 10 | render() { 11 | return null; 12 | } 13 | } 14 | 15 | export default Callback; 16 | -------------------------------------------------------------------------------- /frontend/src/components/NaviBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class NaviBar extends Component { 4 | constructor(props){ 5 | super(props); 6 | this.login = this.login.bind(this); 7 | this.logout = this.logout.bind(this); 8 | } 9 | login() { 10 | this.props.auth.login(); 11 | } 12 | 13 | logout() { 14 | this.props.auth.logout(); 15 | } 16 | render(){ 17 | const {isAuthenticated} = this.props.auth; 18 | return 52 | } 53 | } 54 | 55 | export default NaviBar; 56 | -------------------------------------------------------------------------------- /frontend/src/components/Notification.js: -------------------------------------------------------------------------------- 1 | //import React from 'react'; 2 | import {NotificationManager} from 'react-notifications'; 3 | 4 | export default class Notification{ 5 | static showInfo(message, title=null){ 6 | NotificationManager.info(message, title); 7 | } 8 | static showError(message, title=null){ 9 | NotificationManager.error(message, title); 10 | } 11 | static showWarning(message, title=null){ 12 | NotificationManager.warning(message, title); 13 | } 14 | static showSuccess(message, title=null){ 15 | NotificationManager.success(message, title); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/components/SideBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class SideBar extends Component{ 5 | static propTypes = { 6 | match: PropTypes.object.isRequired, 7 | location: PropTypes.object.isRequired, 8 | history: PropTypes.object.isRequired 9 | } 10 | render(){ 11 | const {location}= this.props; 12 | const {isAuthenticated} = this.props.auth; 13 | return 61 | } 62 | } 63 | 64 | export default SideBar; 65 | -------------------------------------------------------------------------------- /frontend/src/components/appointments/AppointmentCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {getAutoAppointment, getCartAppointment, cancelAppointment} from '../../api/student.js'; 3 | import moment from 'moment'; 4 | 5 | export default class AppointmentCard extends Component{ 6 | delete_auto_rows=[]; 7 | delete_cart_rows=[]; 8 | constructor(props){ 9 | super(props); 10 | this.state={onEdit:false, 11 | autoride: [], 12 | cartride: [], 13 | } 14 | this.onClickEdit=this.onClickEdit.bind(this); 15 | this.onClickSubmit=this.onClickSubmit.bind(this); 16 | this.onClickRevert=this.onClickRevert.bind(this); 17 | } 18 | onClickEdit(){ 19 | this.setState({onEdit:true}); 20 | } 21 | onClickSubmit(){ 22 | let cancel_auto_apt_ids=this.delete_auto_rows.map((item)=>this.state.autoride[item].apt_id); 23 | let cancel_cart_apt_ids=this.delete_cart_rows.map((item)=>this.state.cartride[item].apt_id); 24 | let cancel_apt_ids=cancel_cart_apt_ids.concat(cancel_auto_apt_ids); 25 | cancelAppointment(cancel_apt_ids).then( 26 | ()=>window.location.reload() 27 | ); 28 | } 29 | onClickRevert(){ 30 | this.setState({onEdit:false}); 31 | this.delete_auto_rows = []; 32 | this.delete_cart_rows = []; 33 | window.location.reload(); 34 | } 35 | onDeleteAutoAppointment=(row)=>()=>this.delete_auto_rows.push(row); 36 | onDeleteCartAppointment=(row)=>()=>this.delete_cart_rows.push(row); 37 | componentDidMount(){ 38 | let autoride; 39 | let cartride; 40 | getAutoAppointment().then( 41 | (autodata)=>autoride=autodata.data).then( 42 | ()=>getCartAppointment()).then( 43 | (cartdata)=>cartride=cartdata.data).then( 44 | ()=>this.setState({autoride:autoride, cartride:cartride})); 45 | } 46 | render(){ 47 | return
48 |
49 |

Your Appointments

50 |
51 |
52 |
53 |
54 |

Auto Ride Appointments

55 | {!this.state.onEdit? 56 | ():null} 59 | {this.state.autoride.map((item, index)=>( 60 |
61 | {this.state.onEdit?( 62 | 65 | ):null} 66 | You will be picked up at stop {item.pick_up_stop} {item.pick_up_stop_name} at {item.pick_up_time}, {moment(item.date).format('MM/DD/YYYY')} and dropped off at stop {item.drop_off_stop} {item.drop_off_stop_name} 67 |
68 | ))} 69 |
70 |
71 |

Cart Ride Appointments

72 | {!this.state.onEdit? 73 | ():null} 76 | {this.state.cartride.map((item, index)=>( 77 |
78 | {this.state.onEdit?( 79 | 82 | ):null} 83 | You will be picked up at stop {item.pick_up_stop} {item.pick_up_stop_name} at {item.pick_up_time}, {moment(item.date).format('MM/DD/YYYY')} by our driver {item.driver_first_name} {item.driver_last_name} 84 |
85 | ))} 86 |
87 |
88 | {this.state.onEdit?( 89 |
90 |
91 | 92 |
93 |
94 | 95 |
96 |
97 | ):null 98 | } 99 |
100 |
101 | } 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/components/main/MainCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class MainCard extends Component{ 4 | render(){ 5 | return
6 |
7 |
8 |
9 |

ReadMe

10 |

App Develepment Decription

11 |
12 |
13 |
14 |

Overview

15 |

16 | In this website, guest user can lookup two timetables, the campus auto ride and the campus cart ride. In order to make the appontments, 17 | user should log into the system to make the appointment. The login method supports the native register or use 18 | the your previous google account. After login, the user can make new appointments, 19 | lookup and cancel the existing appontments. Also, the user can 20 | update their profile. 21 |

22 |
23 |
24 |

Rules and Assumptions

25 |

All the appontments should be made at least one day ahead.

26 |
About Auto Ride
27 |

28 | Auto ride has the fixed timetable for each business day. Each auto would be scheduled to a driver by the manager so that the timetable of 29 | that vehicle can be queried by the users. Each auto has maximum capacity and can not be overloaded. 30 |

31 |
About Cart Ride
32 |

33 | Cart ride can have requested pick up time and do not need to make the fixed drop off location. Each cart can only have one appointment within 34 | half hour. 35 |

36 |
37 |
38 |
39 |
40 |
41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/student/InfoCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {fetchStudentInfo} from '../../api/student.js' 3 | 4 | class InfoCard extends Component{ 5 | constructor(props){ 6 | super(props); 7 | this.state = { 8 | student_ids: [], 9 | student_info: null 10 | }; 11 | this.onSelect = this.onSelect.bind(this); 12 | } 13 | onSelect=(e)=>{ 14 | let stu_id = e.target.value; 15 | if(this.props.onSelectStudentId) 16 | this.props.onSelectStudentId(stu_id); 17 | fetchStudentInfo(stu_id).then(info=>this.setState({student_info:info})); 18 | } 19 | componentDidMount(){ 20 | fetchStudentInfo().then( 21 | data=>data.data).then( 22 | data=>this.setState({student_ids: data.map((item)=>item.student_id)})); 23 | } 24 | render(){ 25 | return
26 |
27 |

Student Information

28 |
29 |
30 |
31 |
32 |

Student ID

33 | 38 |
39 |
40 |

student_infor

41 |
42 | 43 | {this.state.student_info? 44 | this.state.student_info.fields.map((field)=>

{field}: {this.state.student_info.data[0][field]}

): 45 |
This is student info
46 | } 47 |
48 |
49 |
50 |
51 |
52 |
53 | } 54 | } 55 | 56 | export default InfoCard; 57 | -------------------------------------------------------------------------------- /frontend/src/components/student/ProfileCard.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import {getStudentInfo, updateStudentInfo} from '../../api/student.js'; 4 | 5 | class ProfileCard extends Component{ 6 | constructor(props){ 7 | super(props); 8 | this.state={ email: null, 9 | first_name: null, 10 | last_name: null, 11 | disabled_injured: 0, 12 | cellphone: null} 13 | this.onSubmit = this.onSubmit.bind(this); 14 | } 15 | onChangeInput=(field)=>(e)=> { 16 | let update_state={} 17 | let value = e.target.value; 18 | if(value==='True') value=1; 19 | if(value==='False') value=0; 20 | update_state[field]=value; 21 | this.setState(update_state); 22 | } 23 | onSubmit(){ 24 | updateStudentInfo(this.state); 25 | } 26 | componentDidMount(){ 27 | getStudentInfo().then(data=>this.setState(data.data[0])); 28 | } 29 | render(){ 30 | return
31 |
32 |
33 |
34 |

Edit Profile

35 |

Complete your profile

36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 |
53 |
54 |
55 |
56 | 57 | 58 |
59 |
60 |
61 |
62 |
63 |
64 | 65 | 66 |
67 |
68 |
69 |
70 | 71 | 75 |
76 |
77 |
78 | 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 | React ICO 88 | 89 |
90 |
91 |
Dear {this.state.first_name?this.state.first_name:'user'}
92 |

93 | Please complete or update your profile for the better experience in Campus Ride App. 94 |

95 | Learn More 96 |
97 |
98 |
99 |
100 | } 101 | } 102 | export default ProfileCard; 103 | -------------------------------------------------------------------------------- /frontend/src/components/student/Student.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import InfoCard from './InfoCard.js'; 4 | import {AutoTimeTable, CartTimeTable} from './TimeTable.js'; 5 | import AppointmentCard from './AppointmentCard.js'; 6 | 7 | 8 | class Student extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | select_student_id: null 14 | }; 15 | this.onSelectStudentId = this.onSelectStudentId.bind(this); 16 | } 17 | onSelectStudentId=(stu_id) => { 18 | this.setState({select_student_id: stu_id}); 19 | // this.select_student_id=stu_id; 20 | // console.log(this.state.select_student_id); 21 | } 22 | render() { 23 | return
24 | 25 | 26 | 27 | 28 |
29 | } 30 | } 31 | 32 | export default Student; 33 | -------------------------------------------------------------------------------- /frontend/src/components/student/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/frontend/src/components/student/favicon.ico -------------------------------------------------------------------------------- /frontend/src/components/template/StopSelect.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import {fetchStopInfo} from '../../api/public'; 4 | 5 | class StopSelect extends Component{ 6 | constructor(props){ 7 | super(props); 8 | this.state ={stops: []} 9 | } 10 | componentDidMount(){ 11 | 12 | fetchStopInfo().then(data=> 13 | this.setState({stops: data.data})); 14 | } 15 | render(){ 16 | return 20 | } 21 | } 22 | 23 | export default StopSelect; 24 | -------------------------------------------------------------------------------- /frontend/src/components/template/Table.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Table extends Component { 4 | constructor(props){ 5 | super(props); 6 | this.state = {selectedRow: null}; 7 | this.onClickRow = this.onClickRow.bind(this); 8 | } 9 | onClickRow = (row)=>()=>{ 10 | if(this.props.onClickRow) 11 | this.props.onClickRow(row); 12 | this.setState({selectedRow: row}); 13 | } 14 | shouldComponentUpdate(prevProps, prevState, snapshot){ 15 | this.state.selectedRow = null; 16 | return true; 17 | } 18 | render(){ 19 | return
20 | 21 | 22 | {this.props.fields.map((member)=> )} 23 | 24 | 25 | {this.props.data.map((row, index)=> 26 | 29 | {this.props.fields.map((field)=> 30 | )} 31 | 32 | )} 33 | 34 |
{member}
{row[field]}
35 |
36 | } 37 | } 38 | 39 | export default Table; 40 | -------------------------------------------------------------------------------- /frontend/src/components/timetable/TimeTable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | 4 | import StopSelect from '../template/StopSelect.js'; 5 | import Table from '../template/Table.js'; 6 | import DatePicker from 'react-datepicker'; 7 | import 'react-datepicker/dist/react-datepicker.css'; 8 | import DateTime from 'react-datetime'; 9 | import 'react-datetime/css/react-datetime.css' 10 | 11 | import {fetchTimeTable} from '../../api/public.js'; 12 | import {makeAppointment} from '../../api/student.js'; 13 | 14 | const bindSelectPickStop=(table, refresh=true)=>(e)=>{table.select_pik_stop=e.target.value; if(refresh)table.refreshTimeTable();}; 15 | const bindSelectDropStop=(table, refresh=true)=>(e)=>{table.select_drp_stop=e.target.value; if(refresh)table.refreshTimeTable();}; 16 | const bindSelectTimeTable=(table)=>(row)=>table.setState({select_time_table_row:row}); 17 | const bindSelectDate=(table, refresh=true)=>(date)=>{ table.state.select_date=date; if(refresh)table.refreshTimeTable();table.setState({select_date: date});}; 18 | 19 | export class AutoTimeTable extends Component{ 20 | select_pik_stop = null; 21 | select_drp_stop = null; 22 | constructor(props){ 23 | super(props); 24 | this.state = { 25 | time_table: {fields:[], data:[]}, 26 | select_date: moment().add(1,'days'), 27 | select_time_table_row: null 28 | }; 29 | this.onMakeAppointment = this.onMakeAppointment.bind(this); 30 | this.onSelectPickStop = bindSelectPickStop(this); 31 | this.onSelectDropStop = bindSelectDropStop(this); 32 | this.onSelectDate = bindSelectDate(this); 33 | this.onSelectTimeTable = bindSelectTimeTable(this); 34 | this.login = this.login.bind(this); 35 | } 36 | refreshTimeTable(){ 37 | if (this.state.select_date&&this.select_pik_stop&&this.select_drp_stop){ 38 | fetchTimeTable({ 39 | date: this.state.select_date.format('YYYY-MM-DD'), 40 | pick_up_stop: this.select_pik_stop, 41 | drop_off_stop: this.select_drp_stop 42 | }).then(data =>{ 43 | this.setState({time_table: data, select_time_table_row: null}) 44 | }); 45 | } 46 | } 47 | onMakeAppointment=()=>{ 48 | if (this.state.select_time_table_row){ 49 | const schedule=this.state.time_table.data[this.state.select_time_table_row]; 50 | schedule.pick_up_stop = this.select_pik_stop; 51 | schedule.drop_off_stop = this.select_drp_stop; 52 | makeAppointment(schedule).then(()=>this.refreshTimeTable()); 53 | } 54 | } 55 | login=()=>this.props.auth.login(); 56 | render(){ 57 | const {isAuthenticated} = this.props.auth; 58 | return
59 |
60 |

Auto Ride Time Table

61 |
62 |
63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 |
83 | 88 |
89 |
90 | {isAuthenticated()? 91 | : 92 | 93 | } 94 |
95 |
96 | 97 | 98 | } 99 | }; 100 | 101 | export class CartTimeTable extends Component{ 102 | select_pik_stop = null; 103 | constructor(props){ 104 | super(props); 105 | this.state = { 106 | time_table: {fields:[],data:[]}, 107 | select_date: moment().add(1, 'days'), 108 | select_time_table_row: null, 109 | }; 110 | this.onMakeAppointment = this.onMakeAppointment.bind(this); 111 | this.onSelectPickStop = bindSelectPickStop(this); 112 | this.onSelectDate = bindSelectDate(this); 113 | this.onSelectTimeTable = bindSelectTimeTable(this); 114 | this.login = this.login.bind(this); 115 | } 116 | refreshTimeTable(){ 117 | if (this.state.select_date&&this.select_pik_stop){ 118 | fetchTimeTable({ 119 | datetime: this.state.select_date.format(), 120 | time: this.state.select_date.format('hh:mm'), 121 | date: this.state.select_date.format('YYYY-MM-DD') 122 | }).then(data =>{ 123 | this.setState({time_table: data, select_time_table_row: null})}); 124 | } 125 | } 126 | onMakeAppointment=()=>{ 127 | if (this.state.select_time_table_row&&this.state.select_date){ 128 | const schedule=this.state.time_table.data[this.state.select_time_table_row]; 129 | schedule.pick_up_stop = this.select_pik_stop; 130 | schedule.pick_up_time = this.state.select_date.format('hh:mm'); 131 | makeAppointment(schedule).then(()=>this.refreshTimeTable()); 132 | } 133 | } 134 | login=()=>{ 135 | this.props.auth.login(); 136 | } 137 | render(){ 138 | const {isAuthenticated} = this.props.auth; 139 | return
140 |
141 |

Cart Ride Time Table

142 |
143 |
144 |
145 |
146 | 147 | currentDate.isAfter(moment())}/> 153 |
154 |
155 | 156 | 157 |
158 |
159 |
160 |
161 |
166 |
167 |
168 | {isAuthenticated()? 169 | : 170 | 171 | } 172 |
173 |
174 | 175 | 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /frontend/src/history.js: -------------------------------------------------------------------------------- 1 | import createHistory from 'history/createBrowserHistory' 2 | 3 | export default createHistory(); 4 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // import './index.css'; 4 | import {BrowserRouter, Route, Switch, withRouter, Redirect} from 'react-router-dom'; 5 | 6 | import {NotificationContainer} from 'react-notifications'; 7 | import 'react-notifications/lib/notifications.css'; 8 | import SideBar from './components/SideBar.js'; 9 | import NaviBar from './components/NaviBar.js'; 10 | 11 | import MainCard from './components/main/MainCard.js' 12 | import ProfileCard from './components/student/ProfileCard.js' 13 | import {AutoTimeTable, CartTimeTable} from './components/timetable/TimeTable.js' 14 | import AppointmentCard from './components/appointments/AppointmentCard.js' 15 | import Callback from './components/Callback.js' 16 | import registerServiceWorker from './registerServiceWorker.js'; 17 | 18 | import Auth from './Auth/Auth'; 19 | 20 | const auth=new Auth(); 21 | 22 | ReactDOM.render( 23 |
24 | 25 |
26 | 27 | )}/> 28 |
29 | 30 |
31 | 32 | auth.isAuthenticated()?:}/> 33 | }/> 34 | }/> 35 | auth.isAuthenticated()?:}/> 36 | }/> 37 | }/> 38 | 39 |
40 |
41 |
42 |
43 |
, 44 | document.getElementById('root') 45 | ); 46 | 47 | registerServiceWorker(); 48 | -------------------------------------------------------------------------------- /frontend/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /imgs/CampusRide-EERD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/CampusRide-EERD.png -------------------------------------------------------------------------------- /imgs/MetaData-Appointment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/MetaData-Appointment.png -------------------------------------------------------------------------------- /imgs/MetaData-AutoTimeTable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/MetaData-AutoTimeTable.png -------------------------------------------------------------------------------- /imgs/MetaData-Schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/MetaData-Schedule.png -------------------------------------------------------------------------------- /imgs/MetaData-Vehicle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/MetaData-Vehicle.png -------------------------------------------------------------------------------- /imgs/ScreenShot-AppointmentCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-AppointmentCard.png -------------------------------------------------------------------------------- /imgs/ScreenShot-AutoAppointment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-AutoAppointment.png -------------------------------------------------------------------------------- /imgs/ScreenShot-AutoTimeTable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-AutoTimeTable.png -------------------------------------------------------------------------------- /imgs/ScreenShot-CartAppointment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-CartAppointment.png -------------------------------------------------------------------------------- /imgs/ScreenShot-CartRideTable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-CartRideTable.png -------------------------------------------------------------------------------- /imgs/ScreenShot-LoginWindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-LoginWindow.png -------------------------------------------------------------------------------- /imgs/ScreenShot-ProfileCard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/ScreenShot-ProfileCard.png -------------------------------------------------------------------------------- /imgs/use-case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/use-case.jpg -------------------------------------------------------------------------------- /imgs/use-case.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qychen13/CampusRide-React-Express-MySQL/a9f59670e7d2246fd4db2a1ec6af698bf0ff7a70/imgs/use-case.pdf -------------------------------------------------------------------------------- /mysql-database/test-data-generator/schedule.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | dates = [] 4 | for i in range(31): 5 | if (i+1)%7 < 5: 6 | dates.append(i+1) 7 | drivers = list(range(1, 51)) 8 | drivers = np.array(drivers) 9 | autos = list(range(200, 220)) 10 | carts = list(range(100, 130)) 11 | 12 | 13 | sql = 'insert into Schedule(vehicle_id, driver_id, date) Values' 14 | for d in dates: 15 | date = '\'2018-05-{:02d}\''.format(d) 16 | drivers_d = np.random.permutation(drivers)[:40] 17 | carts_d = np.random.permutation(carts)[:20] 18 | for i, auto in enumerate(autos): 19 | sql+='\n({}, {}, {}),'.format(auto, drivers_d[i], date) 20 | for i, cart in enumerate(carts_d): 21 | sql+='\n({}, {}, {}),'.format(cart, drivers_d[i+20], date) 22 | 23 | with open('schedule.sql', 'w') as f: 24 | f.write(sql) --------------------------------------------------------------------------------