From 952123f0a4da1a280c8923e09aff6ca13b0e814f Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Sun, 16 Apr 2017 17:03:00 +0800 Subject: [PATCH] Running out of time! --- README.md | 15 +- app/calendar.css | 571 ++++++++++++++++++++++++ app/components/addeventdialog.jsx | 7 +- app/components/addeventweeklydialog.jsx | 174 ++++++++ app/components/app.jsx | 13 + app/layouts/main.jsx | 44 +- app/pages/group.jsx | 92 +++- app/pages/groups.jsx | 1 + app/pages/home.jsx | 226 +++++++++- package.json | 2 +- server/api.js | 26 ++ server/database.js | 46 +- server/utils.js | 90 +++- 13 files changed, 1255 insertions(+), 52 deletions(-) create mode 100644 app/calendar.css create mode 100644 app/components/addeventweeklydialog.jsx diff --git a/README.md b/README.md index ca5bbe5..99015e7 100644 --- a/README.md +++ b/README.md @@ -17,19 +17,22 @@ A school event planner and timetable - [x] fake validator for jwt at protected endpoints - [ ] assume user is admin - [x] Create group -- [ ] Create one-off events +- [x] Create one-off events - [ ] Create attachments - [ ] Description - [ ] File -- [ ] Create group CCAs - - [ ] Create CCA schedules -- [ ] Create group mentor +- [x] Create group CCAs + - [x] Create CCA schedules + - [ ] Differentiate CCAs from Mentor Groups +- [x] Create group mentor - [ ] Importable timetables -- [ ] Display events as agenda -- [ ] Display events as calendar +- [x] Display events as agenda +- [x] Display events as calendar - [ ] Create sample data +- [ ] Refactor toolbar mutator for homepage pagination + ## Security Pitfalls - Auth mechanism not verified diff --git a/app/calendar.css b/app/calendar.css new file mode 100644 index 0000000..804d854 --- /dev/null +++ b/app/calendar.css @@ -0,0 +1,571 @@ +:global .calendar { + height: calc(100vh - 112px - 17px * 2); +} + +:global .rbc-btn { + color: inherit; + font: inherit; + margin: 0; +} +:global button.rbc-btn { + overflow: visible; + text-transform: none; + -webkit-appearance: button; + cursor: pointer; +} +:global button[disabled].rbc-btn { + cursor: not-allowed; +} +:global button.rbc-input::-moz-focus-inner { + border: 0; + padding: 0; +} +:global .rbc-calendar { + box-sizing: border-box; + height: 100%; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex-align: stretch; + align-items: stretch; +} +:global .rbc-calendar *, +:global .rbc-calendar *:before, +:global .rbc-calendar *:after { + box-sizing: inherit; +} +:global .rbc-abs-full, +:global .rbc-row-bg { + overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} +:global .rbc-ellipsis, +:global .rbc-event-label, +:global .rbc-row-segment .rbc-event-content, +:global .rbc-show-more { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +:global .rbc-rtl { + direction: rtl; +} +:global .rbc-off-range { + color: #b3b3b3; +} +:global .rbc-header { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0 3px; + text-align: center; + vertical-align: middle; + font-weight: bold; + font-size: 90%; + min-height: 0; +} +:global .rbc-header > a, +:global .rbc-header > a:active, +:global .rbc-header > a:visited { + color: inherit; + text-decoration: none; +} +:global .rbc-row-content { + position: relative; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 4; +} +:global .rbc-today { + background-color: #eaf6ff; +} +:global .rbc-toolbar { + display: -ms-flexbox; + display: flex; + -ms-flex-align: center; + align-items: center; + margin-bottom: 10px; + font-size: 16px; +} +:global .rbc-toolbar .rbc-toolbar-label { + width: 100%; + padding: 0 10px; + text-align: center; +} +:global .rbc-toolbar button { + color: #373a3c; + display: inline-block; + margin: 0; + text-align: center; + vertical-align: middle; + background: none; + background-image: none; + border: 1px solid #ccc; + padding: .375rem 1rem; + border-radius: 4px; + line-height: normal; + white-space: nowrap; +} +:global .rbc-toolbar button:active, +:global .rbc-toolbar button.rbc-active { + background-image: none; + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + background-color: #e6e6e6; + border-color: #adadad; +} +:global .rbc-toolbar button:active:hover, +:global .rbc-toolbar button.rbc-active:hover, +:global .rbc-toolbar button:active:focus, +:global .rbc-toolbar button.rbc-active:focus { + color: #373a3c; + background-color: #d4d4d4; + border-color: #8c8c8c; +} +:global .rbc-toolbar button:focus { + color: #373a3c; + background-color: #e6e6e6; + border-color: #adadad; +} +:global .rbc-toolbar button:hover { + color: #373a3c; + background-color: #e6e6e6; + border-color: #adadad; +} +:global .rbc-btn-group { + display: inline-block; + white-space: nowrap; +} +:global .rbc-btn-group > button:first-child:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +:global .rbc-btn-group > button:last-child:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +:global .rbc-rtl .rbc-btn-group > button:first-child:not(:last-child) { + border-radius: 4px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +:global .rbc-rtl .rbc-btn-group > button:last-child:not(:first-child) { + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +:global .rbc-btn-group > button:not(:first-child):not(:last-child) { + border-radius: 0; +} +:global .rbc-btn-group button + button { + margin-left: -1px; +} +:global .rbc-rtl .rbc-btn-group button + button { + margin-left: 0; + margin-right: -1px; +} +:global .rbc-btn-group + .rbc-btn-group, +:global .rbc-btn-group + button { + margin-left: 10px; +} +:global .rbc-event { + cursor: pointer; + padding: 2px 5px; + background-color: #3174ad; + border-radius: 5px; + color: #fff; +} +:global .rbc-event.rbc-selected { + background-color: #265985; +} +:global .rbc-event-label { + font-size: 80%; +} +:global .rbc-event-overlaps { + box-shadow: -1px 1px 5px 0px rgba(51, 51, 51, 0.5); +} +:global .rbc-event-continues-prior { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +:global .rbc-event-continues-after { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +:global .rbc-event-continues-earlier { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +:global .rbc-event-continues-later { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} +:global .rbc-row { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: row; + flex-direction: row; +} +:global .rbc-row-segment { + padding: 0 1px 1px 1px; +} +:global .rbc-selected-cell { + background-color: rgba(0, 0, 0, 0.1); +} +:global .rbc-show-more { + background-color: rgba(255, 255, 255, 0.3); + z-index: 4; + font-weight: bold; + font-size: 85%; + height: auto; + line-height: normal; + white-space: nowrap; +} +:global .rbc-month-view { + position: relative; + border: 1px solid #DDD; + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex: 1 0 0px; + flex: 1 0 0; + width: 100%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + height: 100%; +} +:global .rbc-month-view .rbc-header { + border-bottom: 1px solid #DDD; +} +:global .rbc-month-view .rbc-header + .rbc-header { + border-left: 1px solid #DDD; +} +:global .rbc-rtl .rbc-month-view .rbc-header + .rbc-header { + border-left-width: 0; + border-right: 1px solid #DDD; +} +:global .rbc-month-header { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: row; + flex-direction: row; +} +:global .rbc-month-row { + display: -ms-flexbox; + display: flex; + position: relative; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: hidden; + height: 100%; +} +:global .rbc-month-row + .rbc-month-row { + border-top: 1px solid #DDD; +} +:global .rbc-date-cell { + padding-right: 5px; + text-align: right; +} +:global .rbc-date-cell.rbc-now { + font-weight: bold; +} +:global .rbc-date-cell > a, +:global .rbc-date-cell > a:active, +:global .rbc-date-cell > a:visited { + color: inherit; + text-decoration: none; +} +:global .rbc-row-bg { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: row; + flex-direction: row; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: hidden; +} +:global .rbc-day-bg + .rbc-day-bg { + border-left: 1px solid #DDD; +} +:global .rbc-rtl .rbc-day-bg + .rbc-day-bg { + border-left-width: 0; + border-right: 1px solid #DDD; +} +:global .rbc-overlay { + position: absolute; + z-index: 5; + border: 1px solid #e5e5e5; + background-color: #fff; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.25); + padding: 10px; +} +:global .rbc-overlay > * + * { + margin-top: 1px; +} +:global .rbc-overlay-header { + border-bottom: 1px solid #e5e5e5; + margin: -10px -10px 5px -10px; + padding: 2px 10px; +} +:global .rbc-agenda-view { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex: 1 0 0px; + flex: 1 0 0; + overflow: auto; +} +:global .rbc-agenda-view table { + width: 100%; + border: 1px solid #DDD; +} +:global .rbc-agenda-view table tbody > tr > td { + padding: 5px 10px; + vertical-align: top; +} +:global .rbc-agenda-view table .rbc-agenda-time-cell { + padding-left: 15px; + padding-right: 15px; + text-transform: lowercase; +} +:global .rbc-agenda-view table tbody > tr > td + td { + border-left: 1px solid #DDD; +} +:global .rbc-rtl .rbc-agenda-view table tbody > tr > td + td { + border-left-width: 0; + border-right: 1px solid #DDD; +} +:global .rbc-agenda-view table tbody > tr + tr { + border-top: 1px solid #DDD; +} +:global .rbc-agenda-view table thead > tr > th { + padding: 3px 5px; + text-align: left; + border-bottom: 1px solid #DDD; +} +:global .rbc-rtl .rbc-agenda-view table thead > tr > th { + text-align: right; +} +:global .rbc-agenda-time-cell { + text-transform: lowercase; +} +:global .rbc-agenda-time-cell .rbc-continues-after:after { + content: ' »'; +} +:global .rbc-agenda-time-cell .rbc-continues-prior:before { + content: '« '; +} +:global .rbc-agenda-date-cell, +:global .rbc-agenda-time-cell { + white-space: nowrap; +} +:global .rbc-agenda-event-cell { + width: 100%; +} +:global .rbc-time-column { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + min-height: 100%; +} +:global .rbc-time-column .rbc-timeslot-group { + -ms-flex: 1; + flex: 1; +} +:global .rbc-timeslot-group { + border-bottom: 1px solid #DDD; + min-height: 40px; + display: -ms-flexbox; + display: flex; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; +} +:global .rbc-time-gutter, +:global .rbc-header-gutter { + -ms-flex: none; + flex: none; +} +:global .rbc-label { + padding: 0 5px; +} +:global .rbc-day-slot { + position: relative; +} +:global .rbc-day-slot .rbc-event { + border: 1px solid #265985; + display: -ms-flexbox; + display: flex; + max-height: 100%; + -ms-flex-flow: column wrap; + flex-flow: column wrap; + -ms-flex-align: start; + align-items: flex-start; + overflow: hidden; +} +:global .rbc-day-slot .rbc-event-label { + -ms-flex: none; + flex: none; + padding-right: 5px; + width: auto; +} +:global .rbc-day-slot .rbc-event-content { + width: 100%; + -ms-flex: 1 1 0px; + flex: 1 1 0; + word-wrap: break-word; + line-height: 1; + height: 100%; + min-height: 1em; +} +:global .rbc-day-slot .rbc-time-slot { + border-top: 1px solid #f7f7f7; +} +:global .rbc-time-slot { + -ms-flex: 1 0 0px; + flex: 1 0 0; +} +:global .rbc-time-slot.rbc-now { + font-weight: bold; +} +:global .rbc-day-header { + text-align: center; +} +:global .rbc-day-slot .rbc-event { + position: absolute; + z-index: 2; +} +:global .rbc-slot-selection { + z-index: 10; + position: absolute; + cursor: default; + background-color: rgba(0, 0, 0, 0.5); + color: white; + font-size: 75%; + padding: 3px; +} +:global .rbc-time-view { + display: -ms-flexbox; + display: flex; + -ms-flex-direction: column; + flex-direction: column; + -ms-flex: 1; + flex: 1; + width: 100%; + border: 1px solid #DDD; + min-height: 0; +} +:global .rbc-time-view .rbc-time-gutter { + white-space: nowrap; +} +:global .rbc-time-view .rbc-allday-cell { + width: 100%; + position: relative; +} +:global .rbc-time-view .rbc-allday-events { + position: relative; + z-index: 4; +} +:global .rbc-time-view .rbc-row { + min-height: 20px; +} +:global .rbc-time-header { + display: -ms-flexbox; + display: flex; + -ms-flex: 0 0 auto; + flex: 0 0 auto; + -ms-flex-direction: column; + flex-direction: column; +} +:global .rbc-time-header.rbc-overflowing { + border-right: 1px solid #DDD; +} +:global .rbc-rtl .rbc-time-header.rbc-overflowing { + border-right-width: 0; + border-left: 1px solid #DDD; +} +:global .rbc-time-header > .rbc-row > * + * { + border-left: 1px solid #DDD; +} +:global .rbc-rtl .rbc-time-header > .rbc-row > * + * { + border-left-width: 0; + border-right: 1px solid #DDD; +} +:global .rbc-time-header > .rbc-row:first-child { + border-bottom: 1px solid #DDD; +} +:global .rbc-time-header .rbc-gutter-cell { + -ms-flex: none; + flex: none; +} +:global .rbc-time-header > .rbc-gutter-cell + * { + width: 100%; +} +:global .rbc-time-content { + display: -ms-flexbox; + display: flex; + -ms-flex: 1 0 0%; + flex: 1 0 0%; + -ms-flex-align: start; + align-items: flex-start; + width: 100%; + border-top: 2px solid #DDD; + overflow-y: auto; + position: relative; +} +:global .rbc-time-content > .rbc-time-gutter { + -ms-flex: none; + flex: none; +} +:global .rbc-time-content > * + * > * { + border-left: 1px solid #DDD; +} +:global .rbc-rtl .rbc-time-content > * + * > * { + border-left-width: 0; + border-right: 1px solid #DDD; +} +:global .rbc-time-content > .rbc-day-slot { + width: 100%; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +:global .rbc-current-time-indicator { + position: absolute; + z-index: 1; + left: 0; + height: 1px; + background-color: #74ad31; + pointer-events: none; +} +:global .rbc-current-time-indicator::before { + display: block; + position: absolute; + left: -3px; + top: -3px; + content: ' '; + background-color: #74ad31; + border-radius: 50%; + width: 8px; + height: 8px; +} +:global .rbc-rtl .rbc-current-time-indicator::before { + left: 0; + right: -3px; +} + +/* vim: set expandtab ts=2 sw=2: */ diff --git a/app/components/addeventdialog.jsx b/app/components/addeventdialog.jsx index fe3b087..08b751e 100644 --- a/app/components/addeventdialog.jsx +++ b/app/components/addeventdialog.jsx @@ -7,7 +7,7 @@ export default class AddEventDialog extends React.Component { super(props, context); const now = new Date(); this.state = { - group: null, + group: parseInt(props.group, 10), // TODO: make ids type independent in code name: '', start: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8), end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 10), @@ -160,6 +160,11 @@ export default class AddEventDialog extends React.Component { AddEventDialog.propTypes = { onCancel: React.PropTypes.func.isRequired, onDone: React.PropTypes.func.isRequired, + group: React.PropTypes.string, +}; + +AddEventDialog.defaultProps = { + group: null, }; AddEventDialog.contextTypes = { diff --git a/app/components/addeventweeklydialog.jsx b/app/components/addeventweeklydialog.jsx new file mode 100644 index 0000000..d91742c --- /dev/null +++ b/app/components/addeventweeklydialog.jsx @@ -0,0 +1,174 @@ +import React from 'react'; + +import { Dialog, Input, Dropdown, TimePicker } from 'react-toolbox'; + +export default class AddEventWeeklyDialog extends React.Component { + constructor(props, context) { + super(props, context); + const now = new Date(); + this.state = { + group: parseInt(props.group, 10), // TODO: make ids type independent in code + name: '', + day: null, + starttime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8), + endtime: new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10), + days: [ + { value: 0, label: 'Sunday' }, + { value: 1, label: 'Monday' }, + { value: 2, label: 'Tuesday' }, + { value: 3, label: 'Wednesday' }, + { value: 4, label: 'Thursday' }, + { value: 5, label: 'Friday' }, + { value: 6, label: 'Saturday' }, + ], + groups: [], + }; + + this.handleGroupChange = this.handleGroupChange.bind(this); + this.handleNameChange = this.handleNameChange.bind(this); + this.handleDayChange = this.handleDayChange.bind(this); + this.handleStartTimeChange = this.handleStartTimeChange.bind(this); + this.handleEndTimeChange = this.handleEndTimeChange.bind(this); + this.addEvent = this.addEvent.bind(this); + + this.actions = [ + { label: 'Cancel', onClick: this.props.onCancel, accent: true }, + { label: 'Add', onClick: this.addEvent, accent: true }, + ]; + + this.fetchGroups(context); + } + + fetchGroups(context = this.context) { + return fetch(`/api/v1/schools/${context.user.school}/users/${context.user.id}/groups/`, { + headers: { + FakeAuth: true, + FakeID: context.user.id, + }, + }) + .then(data => data.json()) + .then((data) => { + this.setState({ + groups: data.map(g => ({ value: g.id, label: g.name })), + }); + }) + .catch((err) => { + console.error(err); + }); + } + + addEvent() { + const method = 'POST'; + const headers = new Headers(); + headers.append('FakeAuth', 'true'); + headers.append('FakeID', this.context.user.id); + headers.append('Content-Type', 'application/json'); + const body = JSON.stringify({ + name: this.state.name, + day: this.state.day, + starttime: this.state.starttime, + endtime: this.state.endtime, + }); + fetch(`/api/v1/schools/${this.context.user.school}/groups/${this.state.group}/eventsWeekly/`, { + method, headers, body, + }) + .then(() => { + this.props.onDone(); + }) + .catch((err) => { + console.error(err); + }); + } + + handleGroupChange(value) { + this.setState({ + group: value, + }); + } + + handleNameChange(value) { + this.setState({ + name: value, + }); + } + + handleDayChange(value) { + this.setState({ + day: value, + }); + } + + handleStartTimeChange(value) { + this.setState({ + starttime: value, + }); + } + handleEndTimeChange(value) { + this.setState({ + endtime: value, + }); + } + + render() { + return ( + + + + + + + + ); + } +} + +AddEventWeeklyDialog.propTypes = { + onCancel: React.PropTypes.func.isRequired, + onDone: React.PropTypes.func.isRequired, + group: React.PropTypes.string, +}; + +AddEventWeeklyDialog.defaultProps = { + group: null, +}; + +AddEventWeeklyDialog.contextTypes = { + // eslint-disable-next-line react/forbid-prop-types + user: React.PropTypes.object.isRequired, + token: React.PropTypes.string, +}; + diff --git a/app/components/app.jsx b/app/components/app.jsx index b980a06..a6b84da 100644 --- a/app/components/app.jsx +++ b/app/components/app.jsx @@ -10,9 +10,20 @@ import PageGroup from '../pages/group'; export default class App extends React.Component { getChildContext() { + let cb = () => {}; + let pN = () => {}; + let pP = () => {}; return { user: {}, token: null, + tooling: { + setToolbar: (o) => { cb(o); }, + setPaginatePrev: (f) => { pP = f; }, + setPaginateNext: (f) => { pN = f; }, + paginatePrev: () => { pP(); }, + paginateNext: () => { pN(); }, + onChange: (f) => { cb = f; }, + }, }; } @@ -35,4 +46,6 @@ App.childContextTypes = { // eslint-disable-next-line react/forbid-prop-types user: React.PropTypes.object.isRequired, token: React.PropTypes.string, + // eslint-disable-next-line react/forbid-prop-types + tooling: React.PropTypes.object.isRequired, }; diff --git a/app/layouts/main.jsx b/app/layouts/main.jsx index 3f2188f..9f673df 100644 --- a/app/layouts/main.jsx +++ b/app/layouts/main.jsx @@ -3,15 +3,37 @@ import React from 'react'; import { Layout, NavDrawer, Panel, AppBar, Navigation, Link, List, ListItem } from 'react-toolbox'; export default class LayoutMain extends React.Component { - constructor(props) { - super(props); + constructor(props, context) { + super(props, context); this.state = { drawerActive: false, + title: 'Chronos', + showPagination: false, }; + this.context = context; + + // eslint-disable-next-line no-param-reassign + this.context.tooling.onChange(this.setToolbar.bind(this)); + this.paginatePrev = this.paginatePrev.bind(this); + this.paginateNext = this.paginateNext.bind(this); this.toggleDrawerActive = this.toggleDrawerActive.bind(this); } + setToolbar(o) { + this.setState({ + title: o.title || 'Chronos', + showPagination: o.showPagination, + }); + } + + paginatePrev() { + this.context.tooling.onPaginatePrev(); + } + paginateNext() { + this.context.tooling.onPaginateNext(); + } + toggleDrawerActive() { this.setState({ drawerActive: !this.state.drawerActive, // TODO: use function instead @@ -73,13 +95,27 @@ export default class LayoutMain extends React.Component { + {this.state.showPagination && + + } + {this.state.showPagination && + + } {this.context.user.email ? 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday'.split(' ')[d]; export default class PageGroup extends React.Component { constructor(props, context) { super(props, context); this.state = { - id: parseInt(props.match.params.id, 10), + id: props.match.params.id, group: {}, - addGroupDialogActive: false, + addEventDialogActive: false, + addEventWeeklyDialogActive: false, }; + this.showAddEventDialog = this.showAddEventDialog.bind(this); + this.hideAddEventDialog = this.hideAddEventDialog.bind(this); + this.showAddEventWeeklyDialog = this.showAddEventWeeklyDialog.bind(this); + this.hideAddEventWeeklyDialog = this.hideAddEventWeeklyDialog.bind(this); + this.fetchGroup(context); // TODO: split into three backend calls } @@ -32,6 +44,30 @@ export default class PageGroup extends React.Component { }); } + showAddEventDialog() { + this.setState({ + addEventDialogActive: true, + }); + } + + hideAddEventDialog() { + this.setState({ + addEventDialogActive: false, + }); + } + + showAddEventWeeklyDialog() { + this.setState({ + addEventWeeklyDialogActive: true, + }); + } + + hideAddEventWeeklyDialog() { + this.setState({ + addEventWeeklyDialogActive: false, + }); + } + render() { return (
@@ -47,13 +83,37 @@ export default class PageGroup extends React.Component { console.log('Hi')} />, + ]} /> ))} - { - // TODO: cca schedule or class timetable - } - - + + + + {this.state.group.eventsWeekly && this.state.group.eventsWeekly.map(e => ( + console.log('Hi')} />, + ]} + /> + ))} + + @@ -64,6 +124,22 @@ export default class PageGroup extends React.Component { /> ))} + +
); } diff --git a/app/pages/groups.jsx b/app/pages/groups.jsx index 7a41619..9a66156 100644 --- a/app/pages/groups.jsx +++ b/app/pages/groups.jsx @@ -60,6 +60,7 @@ export default class PageGroups extends React.Component { this.context.router.history.push(`/groups/${group.id}`)} />, )} diff --git a/app/pages/home.jsx b/app/pages/home.jsx index e62501f..487591c 100644 --- a/app/pages/home.jsx +++ b/app/pages/home.jsx @@ -1,29 +1,164 @@ import React from 'react'; -import { Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox'; -// import BigCalendar from 'react-big-calendar'; -// import moment from 'moment'; -// BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment)); +import { Tabs, Tab, Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox'; + +import BigCalendar from 'react-big-calendar'; +import moment from 'moment-timezone'; +import '../calendar.css'; // Global styles import AddEventDialog from '../components/addeventdialog'; +BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment)); + export default class PageHome extends React.Component { - constructor(props) { - super(props); + constructor(props, context) { + super(props, context); this.state = { - cards: [ - { key: 0, title: 'Math Test', description: 'Meow' }, - ], + events: [], + start: new Date(), + end: new Date(Date.now() + (7 * 24 * 60 * 60 * 1000)), + view: 0, addEventDialogActive: false, + center: new Date(), }; + this.context = context; this.showAddEventDialog = this.showAddEventDialog.bind(this); this.hideAddEventDialog = this.hideAddEventDialog.bind(this); + this.handleViewChange = this.handleViewChange.bind(this); + this.handleNavigateEvent = this.handleNavigateEvent.bind(this); + this.handleSelectEvent = this.handleSelectEvent.bind(this); + + this.fetchEvents(context); // TODO: split into three backend calls } - // eslint-disable-next-line class-methods-use-this - fetchEvents() { - // Do nothing + componentDidMount() { + this.context.tooling.setToolbar({ + showPagination: false, + title: 'This Week\'s Agenda', + }); + this.context.setPaginationNext(this.paginateNext); + this.context.setPaginationPrev(this.paginatePrev); + } + componentWillUnmount() { + this.context.tooling.setToolbar({ + showPagination: false, + }); + this.context.setPaginationNext(() => {}); + this.context.setPaginationPrev(() => {}); + } + + async fetchEvents(context = this.context, start = this.state.start, end = this.state.end) { + const query = `start=${start.getTime()}&end=${end.getTime()}`; + return fetch(`/api/v1/schools/${context.user.school}/users/${context.user.id}/events?${query}`, { + headers: { + FakeAuth: true, + FakeID: context.user.id, + }, + }) + .then(data => data.json()) + .then((data) => { + this.setState({ + events: data.map(e => Object.assign({}, e, { + start: new Date(e.start), + end: new Date(e.end), + })), + }); + console.log(`Fetched! ${data.length} events.`); + }) + .catch((err) => { + console.error(err); + }); + } + + handleViewChange(value, center = this.state.center) { + let start = center; + let end = center; + + if (value === 0) { + start = new Date(); + end = new Date( + Date.now() + (7 * 24 * 60 * 60 * 1000), + ); + this.context.tooling.setToolbar({ + showPagination: false, + title: 'This Week\'s Agenda', + }); + } else if (value === 1) { + start = new Date( + center.getFullYear(), + center.getMonth(), + center.getDate() - center.getDay(), + ); + end = new Date( + center.getFullYear(), + center.getMonth(), + (center.getDate() - center.getDay()) + 7, + ); + this.context.tooling.setToolbar({ + showPagination: true, + title: `${moment(start).format('Do MMM')} to ${moment(end).format('Do MMM Y')}`, + }); + } else if (value === 2) { + start = new Date( + center.getFullYear(), + center.getMonth() - 1, + 23, + ); + end = new Date( + center.getFullYear(), + center.getMonth() + 1, + 6, + ); + this.context.tooling.setToolbar({ + showPagination: true, + title: moment(center).format('MMMM Y'), + }); + } + + if (value !== this.state.value || center !== this.state.center) { + console.log(`Will fetch... ${start} to ${end}`); + this.fetchEvents(this.context, start, end); + } + + this.setState({ + view: value, + start, + end, + center, + }); + } + + paginateNext() { + if (this.state.view === 1) { + this.handleNavigateEvent(new Date(this.state.center.getTime() + (7 * 24 * 60 * 60 * 1000))); + } else if (this.state.view === 2) { + this.handleNavigateEvent(new Date( + this.state.center.getFullYear(), + this.state.center.getMonth() + 1, + )); + } + } + paginatePrev() { + if (this.state.view === 1) { + this.handleNavigateEvent(new Date(this.state.center.getTime() - (7 * 24 * 60 * 60 * 1000))); + } else if (this.state.view === 2) { + this.handleNavigateEvent(new Date( + this.state.center.getFullYear(), + this.state.center.getMonth() - 1, + )); + } + } + + handleNavigateEvent(date) { + this.setState({ + center: date, + }); + this.handleViewChange(this.state.view, date); + } + + handleSelectEvent(event) { + this.context.router.history.push(`/events/${event.id}`); } showAddEventDialog() { @@ -41,17 +176,49 @@ export default class PageHome extends React.Component { render() { return (
-
- {this.state.cards.map(card => - - - {card.description} - -
+ + + {this.state.events.map(event => + + + {`${moment(event.start).format('dddd, MMMM Do YYYY, HH:mm')} to ${moment(event.end).format('MMMM Do, HH:mm')}`} + +