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
|
- [x] fake validator for jwt at protected endpoints
|
||||||
- [ ] assume user is admin
|
- [ ] assume user is admin
|
||||||
- [x] Create group
|
- [x] Create group
|
||||||
- [ ] Create one-off events
|
- [x] Create one-off events
|
||||||
- [ ] Create attachments
|
- [ ] Create attachments
|
||||||
- [ ] Description
|
- [ ] Description
|
||||||
- [ ] File
|
- [ ] File
|
||||||
- [ ] Create group CCAs
|
- [x] Create group CCAs
|
||||||
- [ ] Create CCA schedules
|
- [x] Create CCA schedules
|
||||||
- [ ] Create group mentor
|
- [ ] Differentiate CCAs from Mentor Groups
|
||||||
|
- [x] Create group mentor
|
||||||
- [ ] Importable timetables
|
- [ ] Importable timetables
|
||||||
- [ ] Display events as agenda
|
- [x] Display events as agenda
|
||||||
- [ ] Display events as calendar
|
- [x] Display events as calendar
|
||||||
|
|
||||||
- [ ] Create sample data
|
- [ ] Create sample data
|
||||||
|
|
||||||
|
- [ ] Refactor toolbar mutator for homepage pagination
|
||||||
|
|
||||||
## Security Pitfalls
|
## Security Pitfalls
|
||||||
|
|
||||||
- Auth mechanism not verified
|
- 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);
|
super(props, context);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
this.state = {
|
this.state = {
|
||||||
group: null,
|
group: parseInt(props.group, 10), // TODO: make ids type independent in code
|
||||||
name: '',
|
name: '',
|
||||||
start: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8),
|
start: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8),
|
||||||
end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 10),
|
end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 10),
|
||||||
|
@ -160,6 +160,11 @@ export default class AddEventDialog extends React.Component {
|
||||||
AddEventDialog.propTypes = {
|
AddEventDialog.propTypes = {
|
||||||
onCancel: React.PropTypes.func.isRequired,
|
onCancel: React.PropTypes.func.isRequired,
|
||||||
onDone: React.PropTypes.func.isRequired,
|
onDone: React.PropTypes.func.isRequired,
|
||||||
|
group: React.PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
AddEventDialog.defaultProps = {
|
||||||
|
group: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
AddEventDialog.contextTypes = {
|
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 {
|
export default class App extends React.Component {
|
||||||
getChildContext() {
|
getChildContext() {
|
||||||
|
let cb = () => {};
|
||||||
|
let pN = () => {};
|
||||||
|
let pP = () => {};
|
||||||
return {
|
return {
|
||||||
user: {},
|
user: {},
|
||||||
token: null,
|
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
|
// eslint-disable-next-line react/forbid-prop-types
|
||||||
user: React.PropTypes.object.isRequired,
|
user: React.PropTypes.object.isRequired,
|
||||||
token: React.PropTypes.string,
|
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';
|
import { Layout, NavDrawer, Panel, AppBar, Navigation, Link, List, ListItem } from 'react-toolbox';
|
||||||
|
|
||||||
export default class LayoutMain extends React.Component {
|
export default class LayoutMain extends React.Component {
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
drawerActive: false,
|
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);
|
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() {
|
toggleDrawerActive() {
|
||||||
this.setState({
|
this.setState({
|
||||||
drawerActive: !this.state.drawerActive, // TODO: use function instead
|
drawerActive: !this.state.drawerActive, // TODO: use function instead
|
||||||
|
@ -73,13 +95,27 @@ export default class LayoutMain extends React.Component {
|
||||||
</NavDrawer>
|
</NavDrawer>
|
||||||
<Panel>
|
<Panel>
|
||||||
<AppBar
|
<AppBar
|
||||||
title="Chronos"
|
title={this.state.title}
|
||||||
leftIcon="menu"
|
leftIcon="menu"
|
||||||
onLeftIconClick={this.toggleDrawerActive}
|
onLeftIconClick={this.toggleDrawerActive}
|
||||||
>
|
>
|
||||||
<Navigation
|
<Navigation
|
||||||
type="horizontal"
|
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 ?
|
{this.context.user.email ?
|
||||||
<Link
|
<Link
|
||||||
style={{ color: 'var(--color-dark-contrast)' }}
|
style={{ color: 'var(--color-dark-contrast)' }}
|
||||||
|
@ -119,5 +155,7 @@ LayoutMain.contextTypes = {
|
||||||
// eslint-disable-next-line react/forbid-prop-types
|
// eslint-disable-next-line react/forbid-prop-types
|
||||||
user: React.PropTypes.object.isRequired,
|
user: React.PropTypes.object.isRequired,
|
||||||
token: React.PropTypes.string,
|
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 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 {
|
export default class PageGroup extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
id: parseInt(props.match.params.id, 10),
|
id: props.match.params.id,
|
||||||
group: {},
|
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
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
|
@ -47,13 +83,37 @@ export default class PageGroup extends React.Component {
|
||||||
<ListItem
|
<ListItem
|
||||||
key={e.id}
|
key={e.id}
|
||||||
caption={e.name}
|
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')} />,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{
|
<ListItem
|
||||||
// TODO: cca schedule or class timetable
|
caption="Create a one-time event"
|
||||||
}
|
leftIcon="add"
|
||||||
</List>
|
onClick={this.showAddEventDialog}
|
||||||
<List>
|
/>
|
||||||
|
<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
|
<ListSubHeader
|
||||||
caption="Members"
|
caption="Members"
|
||||||
/>
|
/>
|
||||||
|
@ -64,6 +124,22 @@ export default class PageGroup extends React.Component {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</List>
|
</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>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default class PageGroups extends React.Component {
|
||||||
<ListItem
|
<ListItem
|
||||||
key={group.id}
|
key={group.id}
|
||||||
caption={group.name}
|
caption={group.name}
|
||||||
|
legend="X upcoming events, X members"
|
||||||
onClick={() => this.context.router.history.push(`/groups/${group.id}`)}
|
onClick={() => this.context.router.history.push(`/groups/${group.id}`)}
|
||||||
/>,
|
/>,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,29 +1,164 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox';
|
import { Tabs, Tab, Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox';
|
||||||
// import BigCalendar from 'react-big-calendar';
|
|
||||||
// import moment from 'moment';
|
import BigCalendar from 'react-big-calendar';
|
||||||
// BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment));
|
import moment from 'moment-timezone';
|
||||||
|
import '../calendar.css'; // Global styles
|
||||||
|
|
||||||
import AddEventDialog from '../components/addeventdialog';
|
import AddEventDialog from '../components/addeventdialog';
|
||||||
|
|
||||||
|
BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment));
|
||||||
|
|
||||||
export default class PageHome extends React.Component {
|
export default class PageHome extends React.Component {
|
||||||
constructor(props) {
|
constructor(props, context) {
|
||||||
super(props);
|
super(props, context);
|
||||||
this.state = {
|
this.state = {
|
||||||
cards: [
|
events: [],
|
||||||
{ key: 0, title: 'Math Test', description: 'Meow' },
|
start: new Date(),
|
||||||
],
|
end: new Date(Date.now() + (7 * 24 * 60 * 60 * 1000)),
|
||||||
|
view: 0,
|
||||||
addEventDialogActive: false,
|
addEventDialogActive: false,
|
||||||
|
center: new Date(),
|
||||||
};
|
};
|
||||||
|
this.context = context;
|
||||||
|
|
||||||
this.showAddEventDialog = this.showAddEventDialog.bind(this);
|
this.showAddEventDialog = this.showAddEventDialog.bind(this);
|
||||||
this.hideAddEventDialog = this.hideAddEventDialog.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
|
componentDidMount() {
|
||||||
fetchEvents() {
|
this.context.tooling.setToolbar({
|
||||||
// Do nothing
|
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() {
|
showAddEventDialog() {
|
||||||
|
@ -41,17 +176,49 @@ export default class PageHome extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<div style={{ padding: '1em' }}>
|
<Tabs index={this.state.view} onChange={this.handleViewChange} inverse>
|
||||||
{this.state.cards.map(card =>
|
<Tab label="Agenda">
|
||||||
<Card key={card.key} style={{ margin: '1em', width: 'auto' }}>
|
{this.state.events.map(event =>
|
||||||
<CardTitle title={card.title} />
|
<Card key={`${event.id}@${event.start}`} style={{ margin: '1em', width: 'auto' }}>
|
||||||
<CardText>{card.description}</CardText>
|
<CardTitle title={event.name} />
|
||||||
<CardActions>
|
<CardText>{`${moment(event.start).format('dddd, MMMM Do YYYY, HH:mm')} to ${moment(event.end).format('MMMM Do, HH:mm')}`}</CardText>
|
||||||
<Button label="Edit" accent />
|
<CardActions>
|
||||||
</CardActions>
|
<Button label="Edit" accent />
|
||||||
</Card>,
|
</CardActions>
|
||||||
)}
|
</Card>,
|
||||||
</div>
|
)}
|
||||||
|
</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} />
|
<Button style={{ position: 'fixed', bottom: '1em', right: '1em' }} icon="add" floating accent onClick={this.showAddEventDialog} />
|
||||||
<AddEventDialog
|
<AddEventDialog
|
||||||
active={this.state.addEventDialogActive}
|
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",
|
"express": "^4.14.1",
|
||||||
"jsonwebtoken": "^7.3.0",
|
"jsonwebtoken": "^7.3.0",
|
||||||
"jwk-to-pem": "^1.2.6",
|
"jwk-to-pem": "^1.2.6",
|
||||||
"moment": "^2.18.1",
|
"moment-timezone": "^0.5.13",
|
||||||
"mysql": "^2.13.0",
|
"mysql": "^2.13.0",
|
||||||
"node-fetch": "^1.6.3",
|
"node-fetch": "^1.6.3",
|
||||||
"oidc-client": "^1.3.0-beta.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 { WebError, UnknownError, UnauthenticatedError, NotFoundError, InvalidCredentialsError, BadRequestError } from './errors';
|
||||||
|
|
||||||
|
import { computeOccurrences } from './utils';
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
constructor(database) {
|
constructor(database) {
|
||||||
this.database = database;
|
this.database = database;
|
||||||
|
@ -119,6 +121,13 @@ export default class API {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Events
|
// 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.router.post('/schools/:school/groups/:group/eventsOnce/', this.auth, (req, res, next) => {
|
||||||
this.database.createEventOnce(req.params.school, req.params.group, req.body)
|
this.database.createEventOnce(req.params.school, req.params.group, req.body)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
@ -134,6 +143,23 @@ export default class API {
|
||||||
.catch(next);
|
.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) => {
|
this.router.use('/*', (req, res, next) => {
|
||||||
next(new NotFoundError());
|
next(new NotFoundError());
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import mysql from 'mysql';
|
import mysql from 'mysql';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
import { fatal, getVersion } from './utils';
|
import { fatal, getVersion, stripTimezone } from './utils';
|
||||||
import { NotFoundError, attachNoun } from './errors';
|
import { NotFoundError, attachNoun } from './errors';
|
||||||
|
|
||||||
const DATABASE = 'chronos';
|
const DATABASE = 'chronos';
|
||||||
|
@ -75,7 +75,7 @@ export default class Database {
|
||||||
}
|
}
|
||||||
async getUserGroups(school, id) {
|
async getUserGroups(school, id) {
|
||||||
return this.query(`
|
return this.query(`
|
||||||
SELECT *
|
SELECT group_.*
|
||||||
FROM member, group_
|
FROM member, group_
|
||||||
WHERE member.group_ = group_.id
|
WHERE member.group_ = group_.id
|
||||||
AND member.user = ?
|
AND member.user = ?
|
||||||
|
@ -85,7 +85,7 @@ export default class Database {
|
||||||
|
|
||||||
async getGroups(school) {
|
async getGroups(school) {
|
||||||
return this.query(`
|
return this.query(`
|
||||||
SELECT *
|
SELECT group_.*
|
||||||
FROM user, member, group_
|
FROM user, member, group_
|
||||||
WHERE member.group_ = group_.id
|
WHERE member.group_ = group_.id
|
||||||
AND member.user = user.id
|
AND member.user = user.id
|
||||||
|
@ -130,21 +130,36 @@ export default class Database {
|
||||||
FROM event_once
|
FROM event_once
|
||||||
WHERE event_once.group_ = ?
|
WHERE event_once.group_ = ?
|
||||||
`, [id]);
|
`, [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], {
|
.then(results => Object.assign({}, results[0], {
|
||||||
members: results[1].map(m => Object.assign(m, {
|
members: results[1].map(m => Object.assign(m, {
|
||||||
pwd_hash: undefined,
|
pwd_hash: undefined,
|
||||||
oid_id: undefined,
|
oid_id: undefined,
|
||||||
})),
|
})),
|
||||||
eventsOnce: results[2],
|
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) {
|
async createEventOnce(school, group, data) {
|
||||||
|
console.log(data.starttime);
|
||||||
return this.query(`
|
return this.query(`
|
||||||
INSERT INTO event_once (group_, name, start, end)
|
INSERT INTO event_once (group_, name, start, end)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`, [group, data.name, data.start, data.end]);
|
`, [group, data.name, stripTimezone(data.start), stripTimezone(data.end)]);
|
||||||
}
|
}
|
||||||
async getEventOnce(school, group, id) {
|
async getEventOnce(school, group, id) {
|
||||||
return this.query(`
|
return this.query(`
|
||||||
|
@ -154,6 +169,7 @@ export default class Database {
|
||||||
AND event_once.id = ?
|
AND event_once.id = ?
|
||||||
`, [group, id]);
|
`, [group, id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async getEventClashesWith(school, group, id) {
|
async getEventClashesWith(school, group, id) {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -161,6 +177,26 @@ export default class Database {
|
||||||
|
|
||||||
async getUserEventsBetween(school, user, start, end) {
|
async getUserEventsBetween(school, user, start, end) {
|
||||||
// oh shit
|
// 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 = {}) {
|
query(query, values, options = {}) {
|
||||||
|
|
|
@ -1,13 +1,93 @@
|
||||||
const getVersion = () => {
|
import moment from 'moment-timezone';
|
||||||
|
|
||||||
|
export function getVersion() {
|
||||||
if (process.env.npm_package_version) {
|
if (process.env.npm_package_version) {
|
||||||
return process.env.npm_package_version;
|
return process.env.npm_package_version;
|
||||||
}
|
}
|
||||||
throw new Error('Unable to obtain running version');
|
throw new Error('Unable to obtain running version');
|
||||||
};
|
}
|
||||||
|
|
||||||
const fatal = (e) => {
|
export function fatal(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
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