Running out of time!
parent
82ffd339bc
commit
952123f0a4
15
README.md
15
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
|
||||
|
|
|
@ -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: */
|
|
@ -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 = {
|
||||
|
|
|
@ -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 (
|
||||
<Dialog
|
||||
{...this.props}
|
||||
actions={this.actions}
|
||||
title="Create a weekly event"
|
||||
>
|
||||
<Dropdown
|
||||
auto
|
||||
label="Group"
|
||||
source={this.state.groups}
|
||||
value={this.state.group}
|
||||
required
|
||||
onChange={this.handleGroupChange}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
value={this.state.name}
|
||||
required
|
||||
onChange={this.handleNameChange}
|
||||
/>
|
||||
<Dropdown
|
||||
auto
|
||||
label="Day"
|
||||
source={this.state.days}
|
||||
value={this.state.day}
|
||||
required
|
||||
onChange={this.handleDayChange}
|
||||
/>
|
||||
<TimePicker
|
||||
label="Start Time"
|
||||
value={this.state.starttime}
|
||||
required
|
||||
onChange={this.handleStartTimeChange}
|
||||
/>
|
||||
<TimePicker
|
||||
label="End Time"
|
||||
value={this.state.endtime}
|
||||
required
|
||||
onChange={this.handleEndTimeChange}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 {
|
|||
</NavDrawer>
|
||||
<Panel>
|
||||
<AppBar
|
||||
title="Chronos"
|
||||
title={this.state.title}
|
||||
leftIcon="menu"
|
||||
onLeftIconClick={this.toggleDrawerActive}
|
||||
>
|
||||
<Navigation
|
||||
type="horizontal"
|
||||
>
|
||||
{this.state.showPagination &&
|
||||
<Link
|
||||
style={{ color: 'var(--color-dark-contrast)' }}
|
||||
icon="navigate_before"
|
||||
onClick={this.paginatePrev}
|
||||
/>
|
||||
}
|
||||
{this.state.showPagination &&
|
||||
<Link
|
||||
style={{ color: 'var(--color-dark-contrast)' }}
|
||||
icon="navigate_next"
|
||||
onClick={this.paginateNext}
|
||||
/>
|
||||
}
|
||||
{this.context.user.email ?
|
||||
<Link
|
||||
style={{ color: 'var(--color-dark-contrast)' }}
|
||||
|
@ -119,5 +155,7 @@ LayoutMain.contextTypes = {
|
|||
// 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,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
import { List, ListSubHeader, ListItem } from 'react-toolbox';
|
||||
import { List, ListSubHeader, ListItem, ListDivider, FontIcon } from 'react-toolbox';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import AddEventDialog from '../components/addeventdialog';
|
||||
import AddEventWeeklyDialog from '../components/addeventweeklydialog';
|
||||
|
||||
const getDay = d => '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 (
|
||||
<main>
|
||||
|
@ -47,13 +83,37 @@ export default class PageGroup extends React.Component {
|
|||
<ListItem
|
||||
key={e.id}
|
||||
caption={e.name}
|
||||
legend={`${moment(e.start).format('dddd, MMMM Do YYYY, HH:mm')} to ${moment(e.end).format('MMMM Do, HH:mm')}`}
|
||||
rightActions={[
|
||||
<FontIcon key={0} value="delete" onClick={() => console.log('Hi')} />,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
{
|
||||
// TODO: cca schedule or class timetable
|
||||
}
|
||||
</List>
|
||||
<List>
|
||||
<ListItem
|
||||
caption="Create a one-time event"
|
||||
leftIcon="add"
|
||||
onClick={this.showAddEventDialog}
|
||||
/>
|
||||
<ListDivider />
|
||||
<ListSubHeader
|
||||
caption="Weekly Events"
|
||||
/>
|
||||
{this.state.group.eventsWeekly && this.state.group.eventsWeekly.map(e => (
|
||||
<ListItem
|
||||
key={e.id}
|
||||
caption={e.name}
|
||||
legend={`${getDay(e.day)}, ${e.starttime.slice(0, -3)} to ${e.endtime.slice(0, -3)}`}
|
||||
rightActions={[
|
||||
<FontIcon key={0} value="delete" onClick={() => console.log('Hi')} />,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
<ListItem
|
||||
caption="Create a weekly event"
|
||||
leftIcon="add"
|
||||
onClick={this.showAddEventWeeklyDialog}
|
||||
/>
|
||||
<ListDivider />
|
||||
<ListSubHeader
|
||||
caption="Members"
|
||||
/>
|
||||
|
@ -64,6 +124,22 @@ export default class PageGroup extends React.Component {
|
|||
/>
|
||||
))}
|
||||
</List>
|
||||
<AddEventDialog
|
||||
group={this.state.id}
|
||||
active={this.state.addEventDialogActive}
|
||||
onCancel={this.hideAddEventDialog}
|
||||
onDone={this.fetchGroup}
|
||||
onEscKeyDown={this.hideAddEventDialog}
|
||||
onOverlayClick={this.hideAddEventDialog}
|
||||
/>
|
||||
<AddEventWeeklyDialog
|
||||
group={this.state.id}
|
||||
active={this.state.addEventWeeklyDialogActive}
|
||||
onCancel={this.hideAddEventWeeklyDialog}
|
||||
onDone={this.fetchGroup}
|
||||
onEscKeyDown={this.hideAddEventWeeklyDialog}
|
||||
onOverlayClick={this.hideAddEventWeeklyDialog}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ export default class PageGroups extends React.Component {
|
|||
<ListItem
|
||||
key={group.id}
|
||||
caption={group.name}
|
||||
legend="X upcoming events, X members"
|
||||
onClick={() => this.context.router.history.push(`/groups/${group.id}`)}
|
||||
/>,
|
||||
)}
|
||||
|
|
|
@ -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 (
|
||||
<main>
|
||||
<div style={{ padding: '1em' }}>
|
||||
{this.state.cards.map(card =>
|
||||
<Card key={card.key} style={{ margin: '1em', width: 'auto' }}>
|
||||
<CardTitle title={card.title} />
|
||||
<CardText>{card.description}</CardText>
|
||||
<CardActions>
|
||||
<Button label="Edit" accent />
|
||||
</CardActions>
|
||||
</Card>,
|
||||
)}
|
||||
</div>
|
||||
<Tabs index={this.state.view} onChange={this.handleViewChange} inverse>
|
||||
<Tab label="Agenda">
|
||||
{this.state.events.map(event =>
|
||||
<Card key={`${event.id}@${event.start}`} style={{ margin: '1em', width: 'auto' }}>
|
||||
<CardTitle title={event.name} />
|
||||
<CardText>{`${moment(event.start).format('dddd, MMMM Do YYYY, HH:mm')} to ${moment(event.end).format('MMMM Do, HH:mm')}`}</CardText>
|
||||
<CardActions>
|
||||
<Button label="Edit" accent />
|
||||
</CardActions>
|
||||
</Card>,
|
||||
)}
|
||||
</Tab>
|
||||
<Tab label="Week">
|
||||
<div className="calendar">
|
||||
<BigCalendar
|
||||
view="week"
|
||||
toolbar={false}
|
||||
events={this.state.events}
|
||||
date={this.state.center}
|
||||
startAccessor="start"
|
||||
endAccessor="end"
|
||||
titleAccessor="name"
|
||||
onSelectEvent={this.handleSelectEvent}
|
||||
onNavigate={this.handleNavigateEvent}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab label="Month">
|
||||
<div className="calendar">
|
||||
<BigCalendar
|
||||
view="month"
|
||||
toolbar={false}
|
||||
events={this.state.events}
|
||||
date={this.state.center}
|
||||
startAccessor="start"
|
||||
endAccessor="end"
|
||||
titleAccessor="name"
|
||||
onSelectEvent={this.handleSelectEvent}
|
||||
onNavigate={this.handleNavigateEvent}
|
||||
/>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Button style={{ position: 'fixed', bottom: '1em', right: '1em' }} icon="add" floating accent onClick={this.showAddEventDialog} />
|
||||
<AddEventDialog
|
||||
active={this.state.addEventDialogActive}
|
||||
|
@ -64,3 +231,16 @@ export default class PageHome extends React.Component {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageHome.contextTypes = {
|
||||
router: React.PropTypes.shape({
|
||||
history: React.PropTypes.shape({
|
||||
push: React.PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
// 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,
|
||||
};
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"express": "^4.14.1",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"jwk-to-pem": "^1.2.6",
|
||||
"moment": "^2.18.1",
|
||||
"moment-timezone": "^0.5.13",
|
||||
"mysql": "^2.13.0",
|
||||
"node-fetch": "^1.6.3",
|
||||
"oidc-client": "^1.3.0-beta.3",
|
||||
|
|
|
@ -7,6 +7,8 @@ import jwkToPem from 'jwk-to-pem';
|
|||
|
||||
import { WebError, UnknownError, UnauthenticatedError, NotFoundError, InvalidCredentialsError, BadRequestError } from './errors';
|
||||
|
||||
import { computeOccurrences } from './utils';
|
||||
|
||||
export default class API {
|
||||
constructor(database) {
|
||||
this.database = database;
|
||||
|
@ -119,6 +121,13 @@ export default class API {
|
|||
});
|
||||
|
||||
// Events
|
||||
this.router.post('/schools/:school/groups/:group/eventsWeekly/', this.auth, (req, res, next) => {
|
||||
this.database.createEventWeekly(req.params.school, req.params.group, req.body)
|
||||
.then((data) => {
|
||||
res.json(data);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
this.router.post('/schools/:school/groups/:group/eventsOnce/', this.auth, (req, res, next) => {
|
||||
this.database.createEventOnce(req.params.school, req.params.group, req.body)
|
||||
.then((data) => {
|
||||
|
@ -134,6 +143,23 @@ export default class API {
|
|||
.catch(next);
|
||||
});
|
||||
|
||||
this.router.get('/schools/:school/users/:id/events', this.auth, (req, res, next) => {
|
||||
this.database.getUserEventsBetween(
|
||||
req.params.school,
|
||||
req.params.id,
|
||||
req.query.start,
|
||||
req.query.end,
|
||||
)
|
||||
.then((data) => {
|
||||
const eventsWeekly = computeOccurrences(data.eventsWeekly, req.query.start, req.query.end);
|
||||
res.json(data.eventsOnce
|
||||
.map(e => Object.assign({ type: 'once' }, e))
|
||||
.concat(eventsWeekly)
|
||||
.map(e => Object.assign({ type: 'weekly' }, e)));
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
this.router.use('/*', (req, res, next) => {
|
||||
next(new NotFoundError());
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import mysql from 'mysql';
|
||||
import semver from 'semver';
|
||||
|
||||
import { fatal, getVersion } from './utils';
|
||||
import { fatal, getVersion, stripTimezone } from './utils';
|
||||
import { NotFoundError, attachNoun } from './errors';
|
||||
|
||||
const DATABASE = 'chronos';
|
||||
|
@ -75,7 +75,7 @@ export default class Database {
|
|||
}
|
||||
async getUserGroups(school, id) {
|
||||
return this.query(`
|
||||
SELECT *
|
||||
SELECT group_.*
|
||||
FROM member, group_
|
||||
WHERE member.group_ = group_.id
|
||||
AND member.user = ?
|
||||
|
@ -85,7 +85,7 @@ export default class Database {
|
|||
|
||||
async getGroups(school) {
|
||||
return this.query(`
|
||||
SELECT *
|
||||
SELECT group_.*
|
||||
FROM user, member, group_
|
||||
WHERE member.group_ = group_.id
|
||||
AND member.user = user.id
|
||||
|
@ -130,21 +130,36 @@ export default class Database {
|
|||
FROM event_once
|
||||
WHERE event_once.group_ = ?
|
||||
`, [id]);
|
||||
return Promise.all([getGroup, getMembers, getEventsOnce])
|
||||
const getEventsWeekly = this.query(`
|
||||
SELECT *
|
||||
FROM event_weekly
|
||||
WHERE event_weekly.group_ = ?
|
||||
`, [id]);
|
||||
return Promise.all([getGroup, getMembers, getEventsOnce, getEventsWeekly])
|
||||
.then(results => Object.assign({}, results[0], {
|
||||
members: results[1].map(m => Object.assign(m, {
|
||||
pwd_hash: undefined,
|
||||
oid_id: undefined,
|
||||
})),
|
||||
eventsOnce: results[2],
|
||||
eventsWeekly: results[3],
|
||||
}));
|
||||
}
|
||||
|
||||
async createEventWeekly(school, group, data) {
|
||||
console.log(data.starttime);
|
||||
return this.query(`
|
||||
INSERT INTO event_weekly (group_, name, day, starttime, endtime)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [group, data.name, data.day, stripTimezone(data.starttime), stripTimezone(data.endtime)]);
|
||||
}
|
||||
|
||||
async createEventOnce(school, group, data) {
|
||||
console.log(data.starttime);
|
||||
return this.query(`
|
||||
INSERT INTO event_once (group_, name, start, end)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [group, data.name, data.start, data.end]);
|
||||
`, [group, data.name, stripTimezone(data.start), stripTimezone(data.end)]);
|
||||
}
|
||||
async getEventOnce(school, group, id) {
|
||||
return this.query(`
|
||||
|
@ -154,6 +169,7 @@ export default class Database {
|
|||
AND event_once.id = ?
|
||||
`, [group, id]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
async getEventClashesWith(school, group, id) {
|
||||
// TODO
|
||||
|
@ -161,6 +177,26 @@ export default class Database {
|
|||
|
||||
async getUserEventsBetween(school, user, start, end) {
|
||||
// oh shit
|
||||
const getEventsOnce = this.query(`
|
||||
SELECT event_once.*
|
||||
FROM member, event_once
|
||||
WHERE member.group_ = event_once.group_
|
||||
AND member.user = ?
|
||||
|
||||
AND (
|
||||
(event_once.start >= ? AND event_once.start <= ?)
|
||||
OR
|
||||
(event_once.end >= ? AND event_once.end <= ?)
|
||||
)
|
||||
`, [user, stripTimezone(start), stripTimezone(end), stripTimezone(start), stripTimezone(end)]);
|
||||
const getEventsWeekly = this.query(`
|
||||
SELECT event_weekly.*
|
||||
FROM member, event_weekly
|
||||
WHERE member.group_ = event_weekly.group_
|
||||
AND member.user = ?
|
||||
`, [user]);
|
||||
return Promise.all([getEventsOnce, getEventsWeekly])
|
||||
.then(results => ({ eventsOnce: results[0], eventsWeekly: results[1] }));
|
||||
}
|
||||
|
||||
query(query, values, options = {}) {
|
||||
|
|
|
@ -1,13 +1,93 @@
|
|||
const getVersion = () => {
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
export function getVersion() {
|
||||
if (process.env.npm_package_version) {
|
||||
return process.env.npm_package_version;
|
||||
}
|
||||
throw new Error('Unable to obtain running version');
|
||||
};
|
||||
}
|
||||
|
||||
const fatal = (e) => {
|
||||
export function fatal(e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
};
|
||||
}
|
||||
|
||||
export { getVersion, fatal };
|
||||
export function stripTimezone(date) {
|
||||
// TODO:
|
||||
// move timezone fixes to client side,
|
||||
// let client send dates and times as strings to be directly stored into database
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) {
|
||||
return new Date(parseInt(date, 10));
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
export function computeOccurrences(events, startd, endd) {
|
||||
const SECONDS_IN_WEEK = 7 * 24 * 60 * 60;
|
||||
|
||||
const start = moment(stripTimezone(startd)).tz('Asia/Singapore');
|
||||
const end = moment(stripTimezone(endd)).tz('Asia/Singapore');
|
||||
const eventsOccurrences = [];
|
||||
|
||||
const secondsToPreviousStartOfWeek =
|
||||
-(
|
||||
(start.day() * 24 * 60 * 60) +
|
||||
(start.hour() * 60 * 60) +
|
||||
(start.minute() * 60) +
|
||||
(start.second())
|
||||
);
|
||||
|
||||
const secondsToEnd =
|
||||
moment(end).unix() -
|
||||
moment(start).unix();
|
||||
|
||||
const secondsToNextOccurrence = events.map((e) => {
|
||||
const startT = e.starttime.split(':').map(c => parseInt(c, 10));
|
||||
const endT = e.endtime.split(':').map(c => parseInt(c, 10));
|
||||
const nextStart =
|
||||
(e.day * 24 * 60 * 60) +
|
||||
(startT[0] * 60 * 60) +
|
||||
(startT[1] * 60) +
|
||||
(startT[2]) +
|
||||
secondsToPreviousStartOfWeek;
|
||||
const nextEnd =
|
||||
(e.day * 24 * 60 * 60) +
|
||||
(endT[0] * 60 * 60) +
|
||||
(endT[1] * 60) +
|
||||
(endT[2]) +
|
||||
secondsToPreviousStartOfWeek;
|
||||
return {
|
||||
start: nextStart,
|
||||
end: nextEnd,
|
||||
};
|
||||
});
|
||||
|
||||
secondsToNextOccurrence.forEach((o) => {
|
||||
while (o.end < 0) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
o.start += SECONDS_IN_WEEK;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
o.end += SECONDS_IN_WEEK;
|
||||
}
|
||||
});
|
||||
|
||||
secondsToNextOccurrence.forEach((o, i) => {
|
||||
while (o.start < secondsToEnd) {
|
||||
const nextStart = moment(start).add(o.start, 'seconds');
|
||||
const nextEnd = moment(start).add(o.end, 'seconds');
|
||||
|
||||
eventsOccurrences.push(Object.assign({}, events[i], {
|
||||
start: nextStart,
|
||||
end: nextEnd,
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
o.start += SECONDS_IN_WEEK;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
o.end += SECONDS_IN_WEEK;
|
||||
}
|
||||
});
|
||||
|
||||
return eventsOccurrences;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue