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 @@ + + + + + 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"