diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..b98b1a9
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+# Copy this file to .env before editing any values
+
+# LibCal API 1.1 credentials
+LIBCAL_CLIENT_ID=CHANGEME
+LIBCAL_CLIENT_SECRET=CHANGEME
diff --git a/.gitignore b/.gitignore
index 7ee6115..5a99723 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@ npm-debug.log
# Nuxt generate
dist
+
+# Environment variables via dotenv
+.env
diff --git a/components/SpaceAvailability.vue b/components/SpaceAvailability.vue
new file mode 100644
index 0000000..709ea68
--- /dev/null
+++ b/components/SpaceAvailability.vue
@@ -0,0 +1,130 @@
+
+
+
{{ space }}
+
+
+
+
+
+ -
+ {{ hours.status }}
+ until {{ relativeStatusChange }}
+
+ -
+
+
+ {{ booking.firstName }} {{ booking.lastName[0] }}.
+
+
+ Available
+
+ until closing at {{ relativeStatusChange }}
+
+
+
+
+
+
+
+
diff --git a/data-mocks/libcal-space-avail.json b/data-mocks/libcal-space-avail.json
new file mode 100644
index 0000000..bea17d1
--- /dev/null
+++ b/data-mocks/libcal-space-avail.json
@@ -0,0 +1,74 @@
+[
+ {
+ "bookId": "cs_d49ELk2",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 2939,
+ "fromDate": "2018-03-20T08:00:00-04:00",
+ "toDate": "2018-03-20T11:00:00-04:00",
+ "firstName": "Chris",
+ "lastName": "Terreri",
+ "email": "terreri@nhl.com",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_vp92j77",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 2939,
+ "fromDate": "2018-03-20T11:00:00-04:00",
+ "toDate": "2018-03-20T15:00:00-04:00",
+ "firstName": "Claude",
+ "lastName": "Lemieux",
+ "email": "lemieux@nhl.com",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_m5s8Tyr",
+ "eid": 20089,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-20T12:00:00-04:00",
+ "toDate": "2018-03-20T16:00:00-04:00",
+ "firstName": "Scott",
+ "lastName": "Niedermayer",
+ "email": "niedermayer@nhl.com",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_1OZO2ir",
+ "eid": 20088,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-20T18:00:00-04:00",
+ "toDate": "2018-03-20T20:00:00-04:00",
+ "firstName": "Scott",
+ "lastName": "Stevens",
+ "email": "stevens@nhl.com",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_kwMreUK",
+ "eid": 20089,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-20T19:00:00-04:00",
+ "toDate": "2018-03-20T21:00:00-04:00",
+ "firstName": "Martin",
+ "lastName": "Brodeur",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_1ji9Rgp",
+ "eid": 20088,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-20T22:00:00-04:00",
+ "toDate": "2018-03-21T00:00:00-04:00",
+ "firstName": "Patrick",
+ "lastName": "Elias",
+ "email": "elias@nhl.com",
+ "status": "Confirmed"
+ }
+]
diff --git a/data-mocks/libcal-studyroom-bookings.json b/data-mocks/libcal-studyroom-bookings.json
new file mode 100644
index 0000000..82fa957
--- /dev/null
+++ b/data-mocks/libcal-studyroom-bookings.json
@@ -0,0 +1,146 @@
+[
+ {
+ "bookId": "cs_XnM9XSd",
+ "eid": 20089,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-26T22:00:00-04:00",
+ "toDate": "2018-03-27T00:00:00-04:00",
+ "firstName": "Nick",
+ "lastName": "Cappadona",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_dvrZBU8",
+ "eid": 20088,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-27T08:00:00-04:00",
+ "toDate": "2018-03-27T09:00:00-04:00",
+ "firstName": "Nick",
+ "lastName": "Cappadona",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_x1w6mf2",
+ "eid": 20089,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-27T09:00:00-04:00",
+ "toDate": "2018-03-27T10:00:00-04:00",
+ "firstName": "Ella",
+ "lastName": "Fitzgerald",
+ "email": "nac26@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_zxnYPfO",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 3182,
+ "fromDate": "2018-03-27T09:00:00-04:00",
+ "toDate": "2018-03-27T11:00:00-04:00",
+ "firstName": "Bobby",
+ "lastName": "Bland",
+ "email": "nac26@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_G0L2Vij",
+ "eid": 20088,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-27T10:00:00-04:00",
+ "toDate": "2018-03-27T13:00:00-04:00",
+ "firstName": "Charles",
+ "lastName": "Mingus",
+ "email": "nac26@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_1NJ6esr",
+ "eid": 20089,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-27T14:00:00-04:00",
+ "toDate": "2018-03-27T15:00:00-04:00",
+ "firstName": "Esperanza",
+ "lastName": "Spalding",
+ "email": "nickypasta@gmail.com",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_AaVk3f6",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 3182,
+ "fromDate": "2018-03-27T14:00:00-04:00",
+ "toDate": "2018-03-27T16:00:00-04:00",
+ "firstName": "Cyrus",
+ "lastName": "Chestnut",
+ "email": "nac26@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_9GEaOSB",
+ "eid": 20088,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-27T16:00:00-04:00",
+ "toDate": "2018-03-27T19:00:00-04:00",
+ "firstName": "Art",
+ "lastName": "Blakey",
+ "email": "nickypasta@gmail.com",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_Pz6JbfJ",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 3182,
+ "fromDate": "2018-03-27T18:00:00-04:00",
+ "toDate": "2018-03-27T19:00:00-04:00",
+ "firstName": "Jaco",
+ "lastName": "Pastorius",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_W1OXYHP",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 3182,
+ "fromDate": "2018-03-27T20:00:00-04:00",
+ "toDate": "2018-03-27T21:00:00-04:00",
+ "firstName": "Quincy",
+ "lastName": "Jones",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_mVdBbF3",
+ "eid": 20089,
+ "cid": 5396,
+ "lid": 3182,
+ "fromDate": "2018-03-27T21:00:00-04:00",
+ "toDate": "2018-03-28T00:00:00-04:00",
+ "firstName": "Nina",
+ "lastName": "Simone",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ },
+ {
+ "bookId": "cs_p3A0KUk",
+ "eid": 20087,
+ "cid": 5395,
+ "lid": 3182,
+ "fromDate": "2018-03-27T22:00:00-04:00",
+ "toDate": "2018-03-28T00:00:00-04:00",
+ "firstName": "Shemekia",
+ "lastName": "Copeland",
+ "email": "nick.cappadona@cornell.edu",
+ "status": "Confirmed"
+ }
+]
diff --git a/nuxt.config.js b/nuxt.config.js
index 16761fe..0023ba1 100644
--- a/nuxt.config.js
+++ b/nuxt.config.js
@@ -1,7 +1,60 @@
+// Use .env in nuxt config only (keep secrets on the dl)
+// -- https://github.com/nuxt/nuxt.js/issues/2033#issuecomment-398820574
+// -- https://github.com/nuxt-community/dotenv-module#using-env-file-in-nuxtconfigjs
+require('dotenv').config()
+
+// Body parser middleware needed to inject required fields for LibCal API auth
+const bodyParser = require('body-parser')
+
+const libcalApi = 'https://api2.libcal.com'
+const libcalApiPath = '/api/libcal/'
+const libcalHoursApi = 'https://api3.libcal.com'
+const libcalHoursApiPath = '/api/libcal-hours/'
+
+var restreamClientCreds = (proxyReq, req, res) => {
+ if (req.method === 'POST' && req.body) {
+ // Build object for POST request to obtain access token
+ let body = {
+ client_id: process.env.LIBCAL_CLIENT_ID,
+ client_secret: process.env.LIBCAL_CLIENT_SECRET,
+ grant_type: 'client_credentials'
+ }
+
+ body = JSON.stringify(body)
+
+ // Update headers
+ proxyReq.setHeader('Content-Type', 'application/json')
+ proxyReq.setHeader('Content-Length', Buffer.byteLength(body))
+
+ // Write new body to the proxyReq stream
+ proxyReq.write(body)
+ }
+}
+
module.exports = {
modules: [
'@nuxtjs/axios'
],
+ serverMiddleware: [
+ // Parse request body so it can be manipulated by proxy middleware
+ // -- https://nuxtjs.org/api/configuration-servermiddleware
+ { path: libcalApiPath, handler: bodyParser.json() }
+ ],
+ axios: {
+ prefix: '/api/',
+ proxy: true
+ },
+ proxy: {
+ [libcalApiPath]: {
+ target: libcalApi,
+ pathRewrite: { [`^${libcalApiPath}`]: '' },
+ onProxyReq: restreamClientCreds
+ },
+ [libcalHoursApiPath]: {
+ target: libcalHoursApi,
+ pathRewrite: { [`^${libcalHoursApiPath}`]: '' }
+ }
+ },
/*
** Headers of the page
*/
diff --git a/package.json b/package.json
index 08e7813..8f29fde 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,11 @@
},
"dependencies": {
"@nuxtjs/axios": "^5.1.1",
- "jsonp-promise": "^0.1.2",
+ "body-parser": "^1.18.3",
+ "dotenv": "^6.0.0",
"lodash": "^4.17.4",
- "nuxt": "^1.4.0"
+ "nuxt": "^1.4.0",
+ "unique-string": "^1.0.0"
},
"scripts": {
"dev": "nuxt",
diff --git a/pages/mann/consult/_desk.vue b/pages/_location/consult/_desk.vue
similarity index 82%
rename from pages/mann/consult/_desk.vue
rename to pages/_location/consult/_desk.vue
index 4565ff9..cc8363e 100644
--- a/pages/mann/consult/_desk.vue
+++ b/pages/_location/consult/_desk.vue
@@ -2,10 +2,10 @@
{{ desk.replace('-', ' ') }}
-
{{ deskInfo.description }}
+
{{ hours.description }}
- {{ deskInfo.status }} until
+ {{ hours.status }} until
@@ -33,28 +33,28 @@ export default {
}
},
computed: {
- deskInfo () {
- return this.$store.state.consultDesk
+ hours () {
+ return this.$store.state.hours
},
relativeStatusChange () {
- return Robin.formatFutureOpening(this.deskInfo.statusChange)
+ return Robin.formatStatusChange(this.hours.statusChange)
},
statusClass () {
- return 'status--' + this.deskInfo.status.replace(/\s/g, '-')
+ return 'status--' + this.hours.status.replace(/\s/g, '-')
}
},
async fetch ({ store, params }) {
- await store.dispatch('consultDesk/fetchStatus', {
- desk: params.desk,
- jsonp: false
+ await store.dispatch('hours/fetch', {
+ location: params.desk,
+ desk: true
})
},
mounted () {
// Update desk status every 30 seconds
setInterval(() => {
- this.$store.dispatch('consultDesk/fetchStatus', {
- desk: this.$route.params.desk,
- jsonp: true
+ this.$store.dispatch('hours/fetch', {
+ location: this.$route.params.desk,
+ desk: true
})
}, 1000 * 30)
}
diff --git a/pages/_location/spaces/_category.vue b/pages/_location/spaces/_category.vue
new file mode 100644
index 0000000..fa778ef
--- /dev/null
+++ b/pages/_location/spaces/_category.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
diff --git a/store/consultDesk.js b/store/hours.js
similarity index 60%
rename from store/consultDesk.js
rename to store/hours.js
index f8cb147..e619522 100644
--- a/store/consultDesk.js
+++ b/store/hours.js
@@ -1,4 +1,5 @@
import { assign, isEmpty } from 'lodash'
+import api from '~/utils/libcal-schema'
import Robin from '~/utils/libcal'
export const state = () => ({
@@ -11,35 +12,37 @@ export const mutations = {
}
export const actions = {
- async fetchStatus ({ commit, state }, payload) {
+ async fetch ({ commit, state }, payload) {
// Fetch from LibCal API under any of these scenarios
// -- a. initial request (empty Vuex store)
// -- b. cache has expired
// -- c. the stored change in status has past
// -- d. the clock has struck midnight (we've crossed over to the next day)
if (isEmpty(state) || Robin.staleCache(state.updated) || Robin.pastChange(state.statusChange) || Robin.nextDay(state.updated)) {
- let feed = await Robin.getHours(this.$axios, payload.desk, undefined, payload.jsonp)
-
- // Use bullet delimiter for multi-item description
- let description = Robin.api.desks[payload.desk].description.join(' \u2022 ')
+ let isDesk = typeof payload.desk === 'undefined' ? false : payload.desk
+ let feed = await Robin.getHours(this.$axios, payload.location, undefined, isDesk)
const libcalStatus = feed.locations[0].times.status
const allHours = typeof feed.locations[0].times.hours === 'undefined' ? null : feed.locations[0].times.hours
- const status = await Robin.openNow(this.$axios, payload.desk, libcalStatus, allHours, payload.jsonp)
+ const status = await Robin.openNow(this.$axios, payload.location, libcalStatus, allHours, isDesk)
// Relabel status under certain circumstances
- let statusLabel = Robin.statusLabel(payload.desk, status.current)
+ let statusLabel = Robin.statusLabel(payload.location, status.current)
- const deskData = {
+ const hoursData = {
'name': feed.locations[0].name,
- 'description': description,
'hours': allHours,
'status': statusLabel,
'statusChange': status.change,
'updated': status.timestamp
}
- commit('update', deskData)
+ if (isDesk) {
+ // Use bullet delimiter for multi-item description
+ hoursData['description'] = api.desks[payload.location].description.join(' \u2022 ')
+ }
+
+ commit('update', hoursData)
}
}
}
diff --git a/store/spaces.js b/store/spaces.js
new file mode 100644
index 0000000..8735e0a
--- /dev/null
+++ b/store/spaces.js
@@ -0,0 +1,28 @@
+import { assign, isEmpty } from 'lodash'
+import Robin from '~/utils/libcal'
+
+export const state = () => ({
+})
+
+export const mutations = {
+ prime: (state, data) => data.forEach(d => (state[d.name] = {'id': d.id})),
+ update: (state, data) => assign(state, data)
+}
+
+export const actions = {
+ async fetchSchedule ({ commit, state }, payload) {
+ // Fetch from LibCal API under any of these scenarios
+ // -- a. initial request (empty Vuex store)
+ // -- b. cache has expired
+ // -- c. the stored change in status has past
+ // -- d. the clock has struck midnight (we've crossed over to the next day)
+ if (isEmpty(state) || Robin.staleCache(state.updated) || Robin.pastChange(state.statusChange) || Robin.nextDay(state.updated)) {
+ let feed = await Robin.getReservations(this.$axios, payload.location)
+
+ const schedule = Robin.buildSchedule(feed, payload.location, state, await Robin.openingTime(this.$axios, payload.location), await Robin.closingTime(this.$axios, payload.location))
+
+ commit('update', schedule)
+ }
+ },
+ primeSpaces: ({ commit }, spaces) => commit('prime', spaces)
+}
diff --git a/utils/libcal-schema.js b/utils/libcal-schema.js
new file mode 100644
index 0000000..2d3b1f8
--- /dev/null
+++ b/utils/libcal-schema.js
@@ -0,0 +1,94 @@
+export default {
+ endpoints: {
+ auth: 'libcal/1.1/oauth/token',
+ hours: 'libcal-hours/api_hours_date.php?iid=973&format=json&nocache=1&lid=',
+ spaces: {
+ bookings: 'libcal/1.1/space/bookings?limit=100'
+ }
+ },
+ desks: {
+ career: {
+ hoursId: 7733,
+ description: [
+ 'CALS student services'
+ ]
+ },
+ ciser: {
+ hoursId: 3016,
+ description: []
+ },
+ cscu: {
+ hoursId: 3017,
+ description: [
+ 'statistical consulting'
+ ]
+ },
+ 'cu-career': {
+ hoursId: 7734,
+ description: [
+ 'Graduate',
+ 'Cornell Career Services'
+ ]
+ },
+ elso: {
+ hoursId: 7701,
+ description: [
+ 'english language support'
+ ]
+ },
+ gis: {
+ hoursId: 2204,
+ description: []
+ },
+ gws: {
+ hoursId: 3303,
+ description: [
+ 'writing',
+ 'grad',
+ 'appt only'
+ ]
+ },
+ knight: {
+ hoursId: 3018,
+ description: [
+ 'writing',
+ 'walk-in'
+ ]
+ },
+ rdmsg: {
+ hoursId: 3302,
+ description: [
+ 'research data management'
+ ]
+ },
+ reference: {
+ hoursId: 1710,
+ description: []
+ }
+ },
+ locations: {
+ mann: {
+ id: 96,
+ hoursId: 1707,
+ categories: {
+ studyrooms: {
+ id: false,
+ spaces: [
+ {
+ id: 20087,
+ room: 270
+ },
+ {
+ id: 20088,
+ room: 271
+ },
+ {
+ id: 20089,
+ room: 272
+ }
+ ]
+ }
+ }
+ }
+ }
+}
diff --git a/utils/libcal.js b/utils/libcal.js
index 1c975e6..679e7a0 100644
--- a/utils/libcal.js
+++ b/utils/libcal.js
@@ -1,7 +1,7 @@
+import api from '~/utils/libcal-schema'
+import _ from 'lodash'
import moment from 'moment'
-import jsonpPromise from 'jsonp-promise'
-
-const baseUrl = 'https://api3.libcal.com/'
+import uniqueString from 'unique-string'
// Set formatting strings for Moment's calendar method
// http://momentjs.com/docs/#/customization/calendar
@@ -19,113 +19,163 @@ moment.updateLocale('en', {
}
})
-export default {
- api: {
- endpoints: {
- hours: baseUrl + 'api_hours_date.php?iid=973&format=json&nocache=1&lid='
- },
- desks: {
- career: {
- id: 7733,
- description: [
- 'CALS student services'
- ]
- },
- ciser: {
- id: 3016,
- description: []
- },
- cscu: {
- id: 3017,
- description: [
- 'statistical consulting'
- ]
- },
- 'cu-career': {
- id: 7734,
- description: [
- 'Graduate',
- 'Cornell Career Services'
- ]
- },
- elso: {
- id: 7701,
- description: [
- 'english language support'
- ]
- },
- gis: {
- id: 2204,
- description: []
- },
- gws: {
- id: 3303,
- description: [
- 'writing',
- 'grad',
- 'appt only'
- ]
- },
- knight: {
- id: 3018,
- description: [
- 'writing',
- 'walk-in'
- ]
- },
- rdmsg: {
- id: 3302,
- description: [
- 'research data management'
- ]
- },
- reference: {
- id: 1710,
- description: []
- }
- }
- },
+const libCal = {
timeFormat: 'h:mm a',
alreadyClosed: function (hours) {
if (hours === null) return false
// If multiple openings/closings, compare against last one for the day
- const lastClosing = hours.pop().to
+ let lastClosing = moment(hours.pop().to, libCal.timeFormat)
+
+ lastClosing = libCal.earlyMorningClose(lastClosing)
- return moment().isSameOrAfter(moment(lastClosing, this.timeFormat))
+ return moment().isSameOrAfter(lastClosing)
},
- formatFutureOpening: function (datetime) {
- return datetime === null ? 'no upcoming openings' : moment(datetime).calendar()
+ availableSlot: function (start, end) {
+ return {
+ bookId: 'avail_' + uniqueString(),
+ fromDate: start,
+ toDate: end,
+ isAvailable: true,
+ startTime: libCal.parseDate(start)
+ }
+ },
+ buildSchedule: (bookings, location, spaces, opening, closing) => {
+ let schedule = {}
+ bookings
+ // Only include reservations for requested spaces
+ .filter(b => _.includes(Object.values(spaces).map(s => s.id), b.eid))
+ // Add schedule object for each space
+ .forEach(b => {
+ // Use room name from schema
+ let name = Object.entries(spaces).find(s => s[1].id === b.eid)[0]
+
+ schedule[name] = {
+ id: b.eid,
+ schedule: libCal.bookingsParser(bookings, b.eid, opening, closing)
+ }
+ })
+
+ // Insert 'available until closing' slot for any space with empty schedule
+ Object.keys(spaces).forEach(s => {
+ if (typeof schedule[s] === 'undefined' || !schedule[s].schedule.length) {
+ const availableTilClose = libCal.availableSlot(opening, closing)
+ availableTilClose.lastUp = true
+
+ schedule[s] = {
+ id: spaces[s].id,
+ schedule: [availableTilClose]
+ }
+ }
+ })
+
+ return schedule
+ },
+ requestedSpaces: (location, category) => {
+ const spacesInCategory = api.locations[location].categories[category].spaces
+ let spaces = []
+ spacesInCategory
+ .forEach(s => spaces.push({ 'id': s.id, 'name': s.room }))
+ if (category === 'studyrooms') spaces.reverse()
+ return spaces
+ },
+ bookingsParser: function (bookings, room, openingTime, closingTime) {
+ const roomAvailability = _(bookings)
+ // Filter bookings by room, status(confirmed), and while open
+ .filter(function (booking, index, allBookings) {
+ const confirmed = booking.status === 'Confirmed'
+ const thisRoom = booking.eid === room
+ const whileOpen = moment(booking.fromDate).isSameOrAfter(openingTime) && moment(booking.toDate).isSameOrBefore(closingTime)
+ return thisRoom &&
+ confirmed &&
+ whileOpen
+ })
+ // Sort by start time
+ .sortBy('fromDate')
+ // Fill gaps between & pad bookings with available slots
+ .flatMap(function (booking, index, allBookings) {
+ const paddedBooking = [booking]
+ const prevIndex = index - 1
+ booking.startTime = libCal.parseDate(booking.fromDate)
+
+ // If first booking & starts after opening, pad before
+ if (index === 0 &&
+ moment(booking.fromDate).isAfter(openingTime)
+ ) {
+ const availableNow = libCal.availableSlot(openingTime, booking.fromDate)
+ paddedBooking.splice(0, 0, availableNow)
+ }
+ // If not back-to-back with previous booking, pad before (aka between)
+ if (prevIndex > -1 &&
+ !moment(booking.fromDate).isSame(allBookings[prevIndex].toDate)
+ ) {
+ const availableSlot = libCal.availableSlot(allBookings[prevIndex].toDate, booking.fromDate)
+ paddedBooking.splice(0, 0, availableSlot)
+ }
+ // If final booking...
+ if (index + 1 === allBookings.length) {
+ // Pad after if it falls short of closing
+ if (moment(booking.toDate).isBefore(moment(closingTime))) {
+ const availableTilClose = libCal.availableSlot(booking.toDate, closingTime)
+ availableTilClose.lastUp = true
+ paddedBooking.push(availableTilClose)
+ // Otherwise, mark it as running through to closing
+ } else {
+ booking.lastUp = true
+ }
+ }
+ return paddedBooking
+ })
+ // Now filter out past bookings
+ .filter(function (booking, index, allBookings) {
+ return moment().isSameOrBefore(booking.toDate)
+ })
+ .value()
+
+ return roomAvailability
+ },
+ earlyMorningClose: function (closing) {
+ return moment(closing, libCal.timeFormat).isBefore(moment('6am', libCal.timeFormat)) ? closing.add(1, 'day') : closing
},
formatDate: function (date) {
return moment(date).format('Y-MM-DD')
},
- getHours: function (axios, desk, date, jsonp = false) {
- const requestDate = typeof date === 'undefined' ? '' : '&date=' + this.formatDate(date)
- const url = this.api.endpoints.hours + this.api.desks[desk].id + requestDate
-
- if (jsonp) {
- // If fetching updates from client, need to deal with JSONP
- // -- LibCal doesn't set Access-Control-Allow-Origin header
- return jsonpPromise(url).promise
- } else {
- // Non-issue when proxied through server on initial load (thanks Nuxt)
- return axios.$get(url)
- }
+ formatStatusChange: function (datetime) {
+ const statusChange = datetime === null ? 'no upcoming openings' : moment(datetime).calendar()
+ return statusChange === '12:00 am' ? 'Midnight' : statusChange
+ },
+ formatTime: function (date) {
+ return moment(date).format(libCal.timeFormat)
+ },
+ getHours: function (axios, location, date, isDesk = false) {
+ const requestDate = typeof date === 'undefined' ? '' : '&date=' + libCal.formatDate(date)
+ const locId = isDesk ? api.desks[location].hoursId : api.locations[location].hoursId
+ const url = api.endpoints.hours + locId + requestDate
+
+ return axios.$get(url)
+ },
+ async getReservations (axios, location, date = false) {
+ const requestDate = date ? '&date=' + libCal.formatDate(date) : ''
+ const scope = 'lid=' + api.locations[location].id
+ const url = api.endpoints.spaces.bookings + scope + requestDate
+
+ let authorize = await axios.$post(api.endpoints.auth)
+ axios.setToken(authorize.access_token, 'Bearer')
+
+ return axios.$get(url)
},
nextDay: function (lastUpdated) {
return moment().isAfter(moment(lastUpdated), 'd')
},
- async nextOpening (axios, desk, jsonp = false) {
+ async nextOpening (axios, location, isDesk = false) {
var bigWinner = null
// Check today plus next 14 days
for (var i = 0; i < 15; i++) {
var dateToCheck = moment().add(i, 'days')
- var openingTime = await this.openingTime(axios, desk, this.formatDate(dateToCheck), jsonp)
+ var openingTime = await libCal.openingTime(axios, location, libCal.formatDate(dateToCheck), isDesk)
if (openingTime !== null) {
// Use openingTime to update existing moment and set hours & mins
- openingTime = moment(openingTime, this.timeFormat)
bigWinner = dateToCheck.set({
'hour': openingTime.get('hour'),
'minute': openingTime.get('minute')
@@ -136,22 +186,40 @@ export default {
return bigWinner
},
- async openingTime (axios, desk, date, jsonp = false) {
- let feed = await this.getHours(axios, desk, date, jsonp)
+ async hoursForDate (axios, location, date, isDesk = false) {
+ let feed = await libCal.getHours(axios, location, date, isDesk)
const hours = typeof feed.locations[0].times.hours === 'undefined' ? null : feed.locations[0].times.hours
+ return hours
+ },
+ async openingTime (axios, location, date, isDesk = false) {
+ const hours = await libCal.hoursForDate(axios, location, date, isDesk)
+
// Copy hours since it gets emptied after using as function param
// -- TODO: Consider immutable.js or seamless-immutable
const hoursClone = hours !== null ? hours.slice(0) : null
// If dealing with today, ensure we're not already closed
- if (moment().isSame(moment(date), 'd') && this.alreadyClosed(hoursClone)) {
+ if (moment().isSame(moment(date), 'd') && libCal.alreadyClosed(hoursClone)) {
return null
}
- return hours !== null ? hours[0].from : null
+ return hours !== null ? moment(hours[0].from, libCal.timeFormat) : null
},
- async openNow (axios, desk, libcalStatus, hours, jsonp = false) {
+ async closingTime (axios, location, date, isDesk = false) {
+ const hours = await libCal.hoursForDate(axios, location, date, isDesk)
+
+ let closingTime = hours !== null ? moment(hours[0].to, libCal.timeFormat) : null
+
+ if (closingTime) {
+ // Account for early morning closings the following day
+ // -- LibCal only returns time, no date, so add a day to early morning closings for true comparisons
+ closingTime = libCal.earlyMorningClose(closingTime)
+ }
+
+ return closingTime
+ },
+ async openNow (axios, location, libcalStatus, hours, isDesk = false) {
let status = {
current: 'closed',
timestamp: moment() // Use for caching results from LibCal API
@@ -160,17 +228,20 @@ export default {
if (hours) {
// Account for potential of multiple openings/closings in a given day
const isOpen = hours.find((hoursBlock) => {
- return (moment().isBetween(moment(hoursBlock.from, this.timeFormat), moment(hoursBlock.to, this.timeFormat), null, []))
+ // Account for early morning closings the following day
+ // -- LibCal only returns time, no date, so add a day to early morning closings for true comparisons
+ const closingTime = libCal.earlyMorningClose(moment(hoursBlock.to, libCal.timeFormat))
+ return (moment().isBetween(moment(hoursBlock.from, libCal.timeFormat), closingTime, null, []))
})
if (isOpen !== undefined) {
status.current = 'open'
- status.change = moment(isOpen.to, this.timeFormat)
+ status.change = moment(isOpen.to, libCal.timeFormat)
return status
}
}
- let statusChange = await this.nextOpening(axios, desk, jsonp)
+ let statusChange = await libCal.nextOpening(axios, location, isDesk)
status.change = statusChange
@@ -180,6 +251,14 @@ export default {
return status
},
+ parseDate: function (date) {
+ let startDate = moment(date)
+ let startTime = {}
+ startTime.hour = startDate.format('h')
+ startTime.minute = startDate.format('mm')
+ startTime.meridiem = startDate.format('a')
+ return startTime
+ },
pastChange: function (changeTime) {
return moment().isSameOrAfter(moment(changeTime))
},
@@ -203,3 +282,5 @@ export default {
return status
}
}
+
+export default libCal
diff --git a/yarn.lock b/yarn.lock
index 2d8b280..c202563 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1044,6 +1044,21 @@ body-parser@1.18.2:
raw-body "2.3.2"
type-is "~1.6.15"
+body-parser@^1.18.3:
+ version "1.18.3"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.2"
+ http-errors "~1.6.3"
+ iconv-lite "0.4.23"
+ on-finished "~2.3.0"
+ qs "6.5.2"
+ raw-body "2.3.3"
+ type-is "~1.6.16"
+
boolbase@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -1732,6 +1747,10 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
+crypto-random-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
+
css-color-function@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e"
@@ -2055,6 +2074,10 @@ domutils@1.5.1:
dom-serializer "0"
domelementtype "1"
+dotenv@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.0.0.tgz#24e37c041741c5f4b25324958ebbc34bca965935"
+
duplexer@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
@@ -3163,6 +3186,15 @@ http-errors@1.6.2, http-errors@~1.6.2:
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
+http-errors@1.6.3, http-errors@~1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.4.0 < 2"
+
http-proxy-middleware@^0.17.4:
version "0.17.4"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
@@ -3203,6 +3235,12 @@ iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+iconv-lite@0.4.23:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
icss-replace-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
@@ -3673,10 +3711,6 @@ jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
-jsonp-promise@^0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/jsonp-promise/-/jsonp-promise-0.1.2.tgz#5c4365d9cbee99c79893a9c750a95f6db468997d"
-
jsonpointer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
@@ -5380,6 +5414,10 @@ qs@6.5.1, qs@~6.5.1:
version "6.5.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+qs@6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
qs@~6.3.0:
version "6.3.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
@@ -5436,6 +5474,15 @@ raw-body@2.3.2:
iconv-lite "0.4.19"
unpipe "1.0.0"
+raw-body@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.3"
+ iconv-lite "0.4.23"
+ unpipe "1.0.0"
+
rc@^1.1.7:
version "1.2.6"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.6.tgz#eb18989c6d4f4f162c399f79ddd29f3835568092"
@@ -5848,6 +5895,10 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+
sass-graph@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
@@ -6195,6 +6246,10 @@ static-extend@^0.1.1:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+"statuses@>= 1.4.0 < 2":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+
statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@@ -6621,6 +6676,12 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
+unique-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
+ dependencies:
+ crypto-random-string "^1.0.0"
+
units-css@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07"