aboutsummaryrefslogtreecommitdiff
path: root/src/client/javascript
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/javascript')
-rw-r--r--src/client/javascript/analytics.js35
-rw-r--r--src/client/javascript/autocomplete.js87
-rw-r--r--src/client/javascript/browserFixToolkit.js12
-rw-r--r--src/client/javascript/favorite.js79
-rw-r--r--src/client/javascript/featureDetect.js29
-rw-r--r--src/client/javascript/frontpage.js23
-rw-r--r--src/client/javascript/main.js71
-rw-r--r--src/client/javascript/schedule.js75
-rw-r--r--src/client/javascript/scrollSnap.js59
-rw-r--r--src/client/javascript/search.js95
-rw-r--r--src/client/javascript/url.js67
-rw-r--r--src/client/javascript/weekSelector.js99
-rw-r--r--src/client/javascript/zoom.js30
13 files changed, 761 insertions, 0 deletions
diff --git a/src/client/javascript/analytics.js b/src/client/javascript/analytics.js
new file mode 100644
index 0000000..a93c8a4
--- /dev/null
+++ b/src/client/javascript/analytics.js
@@ -0,0 +1,35 @@
+/* global ga */
+
+const self = {}
+
+self.send = {}
+
+self.send.search = function (selectedUser, favorite) {
+ const hitType = 'event'
+
+ const eventCategory = favorite ? 'search fav' : 'search'
+
+ let eventAction
+ switch (selectedUser.type) {
+ case 'c':
+ eventAction = 'Class'
+ break
+ case 't':
+ eventAction = 'Teacher'
+ break
+ case 'r':
+ eventAction = 'Room'
+ break
+ case 's':
+ eventAction = 'Student'
+ break
+ }
+
+ const eventLabel = selectedUser.value
+
+ ga(function () {
+ ga('send', { hitType, eventCategory, eventAction, eventLabel })
+ })
+}
+
+module.exports = self
diff --git a/src/client/javascript/autocomplete.js b/src/client/javascript/autocomplete.js
new file mode 100644
index 0000000..61f400a
--- /dev/null
+++ b/src/client/javascript/autocomplete.js
@@ -0,0 +1,87 @@
+const EventEmitter = require('events')
+
+const self = new EventEmitter()
+
+self._users = []
+self._selectedUserIndex = -1
+
+self._nodes = {
+ search: document.querySelector('#search'),
+ input: document.querySelector('input[type="search"]'),
+ autocomplete: document.querySelector('.autocomplete')
+}
+
+self.getSelectedUser = function () {
+ if (self.getItems() === []) return
+
+ if (self.getSelectedUserIndex() === -1) {
+ return self.getItems()[0]
+ } else {
+ return self.getItems()[self.getSelectedUserIndex()]
+ }
+}
+
+self.getSelectedUserIndex = function () {
+ return self._selectedUserIndex
+}
+
+self.getItems = function () {
+ return self._users
+}
+
+self.removeAllItems = function () {
+ while (self._nodes.autocomplete.firstChild) {
+ self._nodes.autocomplete.removeChild(self._nodes.autocomplete.firstChild)
+ }
+ self._users = []
+ self._selectedUserIndex = -1
+}
+
+self.addItem = function (user) {
+ const listItem = document.createElement('li')
+ listItem.textContent = user.value
+ self._nodes.autocomplete.appendChild(listItem)
+ self._users.push(user)
+}
+
+self._moveSelected = function (shift) {
+ if (self._selectedUserIndex + shift >= self.getItems().length) {
+ self._selectedUserIndex = -1
+ } else if (self._selectedUserIndex + shift < -1) {
+ self._selectedUserIndex = self.getItems().length - 1
+ } else {
+ self._selectedUserIndex += shift
+ }
+
+ for (let i = 0; i < self.getItems().length; i++) {
+ self._nodes.autocomplete.children[i].classList.remove('selected')
+ }
+ if (self._selectedUserIndex >= 0) {
+ self._nodes.autocomplete
+ .children[self._selectedUserIndex].classList.add('selected')
+ }
+}
+
+self._handleItemClick = function (event) {
+ if (!self._nodes.autocomplete.contains(event.target)) return
+ const userIndex = Array.prototype.indexOf
+ .call(self._nodes.autocomplete.children, event.target)
+ self._selectedUserIndex = userIndex
+ self.emit('select', self.getSelectedUser())
+}
+
+self._handleKeydown = function (event) {
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault()
+ if (event.key === 'ArrowDown') {
+ self._moveSelected(1)
+ } else if (event.key === 'ArrowUp') {
+ self._moveSelected(-1)
+ }
+ }
+}
+
+self._nodes.autocomplete.addEventListener('click', self._handleItemClick)
+self._nodes.input.addEventListener('keydown', self._handleKeydown)
+
+module.exports = self
diff --git a/src/client/javascript/browserFixToolkit.js b/src/client/javascript/browserFixToolkit.js
new file mode 100644
index 0000000..fbeab74
--- /dev/null
+++ b/src/client/javascript/browserFixToolkit.js
@@ -0,0 +1,12 @@
+const self = {}
+
+self.isIE = navigator.userAgent.indexOf('MSIE') !== -1 ||
+ navigator.appVersion.indexOf('Trident/') > 0
+
+if (self.isIE) {
+ self.inputEvent = 'textinput'
+} else {
+ self.inputEvent = 'input'
+}
+
+module.exports = self
diff --git a/src/client/javascript/favorite.js b/src/client/javascript/favorite.js
new file mode 100644
index 0000000..92c87f7
--- /dev/null
+++ b/src/client/javascript/favorite.js
@@ -0,0 +1,79 @@
+/* global USERS */
+
+const EventEmitter = require('events')
+
+const self = new EventEmitter()
+
+self._nodes = {
+ toggle: document.querySelector('.fav')
+}
+
+self.get = function () {
+ try {
+ const localStorageUser = JSON.parse(window.localStorage.getItem('fav'))
+ if (localStorageUser == null) return
+
+ const correctedUser = USERS.filter(function (user) {
+ return user.type === localStorageUser.type &&
+ user.value === localStorageUser.value
+ })[0]
+ return correctedUser
+ } catch (e) {
+ self.delete()
+ return
+ }
+}
+
+self.set = function (user) {
+ window.localStorage.setItem('fav', JSON.stringify(user))
+ self._nodes.innerHTML = '&#xE838;'
+}
+
+self.delete = function () {
+ window.localStorage.removeItem('fav')
+}
+
+self.updateDom = function (isFavorite) {
+ if (isFavorite) {
+ self._nodes.toggle.innerHTML = '&#xE838;'
+ } else {
+ self._nodes.toggle.innerHTML = '&#xE83A'
+ }
+}
+
+self.update = function (selectedUser) {
+ const currentUser = self.get()
+
+ if (currentUser == null || selectedUser == null) {
+ self.updateDom(false)
+ return
+ }
+
+ const isEqual = currentUser.type === selectedUser.type &&
+ currentUser.index === selectedUser.index
+
+ self.updateDom(isEqual)
+}
+
+self.toggle = function (selectedUser) {
+ const currentUser = self.get()
+ const isEqual = currentUser != null &&
+ currentUser.type === selectedUser.type &&
+ currentUser.index === selectedUser.index
+
+ if (isEqual) {
+ self.delete()
+ self.updateDom(false)
+ } else {
+ self.set(selectedUser)
+ self.updateDom(true)
+ }
+}
+
+self._handleClick = function () {
+ self.emit('click')
+}
+
+self._nodes.toggle.addEventListener('click', self._handleClick)
+
+module.exports = self
diff --git a/src/client/javascript/featureDetect.js b/src/client/javascript/featureDetect.js
new file mode 100644
index 0000000..3a072a1
--- /dev/null
+++ b/src/client/javascript/featureDetect.js
@@ -0,0 +1,29 @@
+/* global FLAGS */
+
+const self = {}
+
+self._nodes = {
+ input: document.querySelector('input[type="search"]'),
+ overflowButton: document.querySelector('#overflow-button')
+}
+
+self._shouldCheck = function () {
+ return FLAGS.indexOf('NO_FEATURE_DETECT') === -1
+}
+
+self._redirect = function () {
+ window.location.href = 'http://www.meetingpointmco.nl/Roosters-AL/doc/'
+}
+
+self.check = function () {
+ if (!self._shouldCheck()) return
+
+ window.onerror = self._redirect
+
+ if (self._nodes.input.getClientRects()[0].top !==
+ self._nodes.overflowButton.getClientRects()[0].top) {
+ self._redirect()
+ }
+}
+
+module.exports = self
diff --git a/src/client/javascript/frontpage.js b/src/client/javascript/frontpage.js
new file mode 100644
index 0000000..17cb539
--- /dev/null
+++ b/src/client/javascript/frontpage.js
@@ -0,0 +1,23 @@
+const browserFixToolkit = require('./browserFixToolkit')
+
+const self = {}
+
+self._nodes = {
+ input: document.querySelector('input[type="search"]')
+}
+
+self.isShown = false
+
+self.show = function () {
+ document.body.classList.add('no-input')
+ self.isShown = true
+}
+
+self.hide = function () {
+ document.body.classList.remove('no-input')
+ self.isShown = false
+}
+
+self._nodes.input.addEventListener(browserFixToolkit.inputEvent, self.hide)
+
+module.exports = self
diff --git a/src/client/javascript/main.js b/src/client/javascript/main.js
new file mode 100644
index 0000000..0d125cb
--- /dev/null
+++ b/src/client/javascript/main.js
@@ -0,0 +1,71 @@
+require('./featureDetect').check()
+require('./zoom')
+
+const frontpage = require('./frontpage')
+const search = require('./search')
+const schedule = require('./schedule')
+const weekSelector = require('./weekSelector')
+const favorite = require('./favorite')
+const scrollSnap = require('./scrollSnap')
+const analytics = require('./analytics')
+const url = require('./url')
+
+const state = {}
+
+window.state = state
+
+frontpage.show()
+weekSelector.updateCurrentWeek()
+scrollSnap.startListening()
+
+if (url.hasSelectedUser()) {
+ state.selectedUser = url.getSelectedUser()
+
+ favorite.update(state.selectedUser)
+ url.update(state.selectedUser)
+ analytics.send.search(state.selectedUser)
+
+ schedule.viewItem(weekSelector.getSelectedWeek(), state.selectedUser)
+} else if (favorite.get() != null) {
+ state.selectedUser = favorite.get()
+
+ favorite.update(state.selectedUser)
+ url.push(state.selectedUser, false)
+ url.update(state.selectedUser)
+ analytics.send.search(state.selectedUser, true)
+
+ schedule.viewItem(weekSelector.getSelectedWeek(), state.selectedUser)
+} else {
+ search.focus()
+}
+
+search.on('search', function (selectedUser) {
+ state.selectedUser = selectedUser
+
+ favorite.update(state.selectedUser)
+ url.push(state.selectedUser)
+ url.update(state.selectedUser)
+ analytics.send.search(state.selectedUser)
+
+ schedule.viewItem(weekSelector.getSelectedWeek(), state.selectedUser)
+})
+
+url.on('update', function (selectedUser) {
+ state.selectedUser = selectedUser
+
+ favorite.update(state.selectedUser)
+ url.update(state.selectedUser)
+
+ schedule.viewItem(weekSelector.getSelectedWeek(), state.selectedUser)
+})
+
+weekSelector.on('weekChanged', function (newWeek) {
+ analytics.send.search(state.selectedUser)
+ schedule.viewItem(newWeek, state.selectedUser)
+})
+
+favorite.on('click', function () {
+ favorite.toggle(state.selectedUser)
+})
+
+document.body.style.opacity = 1
diff --git a/src/client/javascript/schedule.js b/src/client/javascript/schedule.js
new file mode 100644
index 0000000..11a2aa8
--- /dev/null
+++ b/src/client/javascript/schedule.js
@@ -0,0 +1,75 @@
+/* global VALID_WEEK_NUMBERS */
+
+const EventEmitter = require('events')
+const search = require('./search')
+
+const self = new EventEmitter()
+
+self._nodes = {
+ schedule: document.querySelector('#schedule')
+}
+
+self._parseMeetingpointHTML = function (htmlStr) {
+ const html = document.createElement('html')
+ html.innerHTML = htmlStr
+ const centerNode = html.querySelector('center')
+ return centerNode
+}
+
+self._handleLoad = function (event) {
+ const request = event.target
+ if (request.status < 200 || request.status >= 400) {
+ self._handleError(event)
+ return
+ }
+ const document = self._parseMeetingpointHTML(request.response)
+ self._removeChilds()
+ self._nodes.schedule.appendChild(document)
+ self._nodes.schedule.classList.remove('error')
+ self.emit('load')
+}
+
+self._handleError = function (event) {
+ const request = event.target
+ let error
+ if (request.status === 404) {
+ error = 'Sorry, er is (nog) geen rooster voor deze week.'
+ } else {
+ error = 'Sorry, er is iets mis gegaan tijdens het laden van deze week.'
+ }
+ self._removeChilds()
+ self._nodes.schedule.textContent = error
+ self._nodes.schedule.classList.add('error')
+ self.emit('load')
+}
+
+self._getURLOfUser = function (week, user) {
+ return `/get/${user.type}/${user.value}?week=${week}`
+}
+
+self._removeChilds = function () {
+ while (self._nodes.schedule.firstChild) {
+ self._nodes.schedule.removeChild(self._nodes.schedule.firstChild)
+ }
+}
+
+self.viewItem = function (week, selectedUser) {
+ if (selectedUser == null) {
+ self._removeChilds()
+ } else if (VALID_WEEK_NUMBERS.indexOf(week) === -1) {
+ self._handleError({ target: { status: 404 } });
+ } else {
+ const url = self._getURLOfUser(week, selectedUser)
+
+ self._removeChilds()
+ const request = new window.XMLHttpRequest()
+ request.addEventListener('load', self._handleLoad)
+ request.addEventListener('error', self._handleError)
+ request.open('GET', url, true)
+ request.send()
+ }
+
+ search.updateDom(selectedUser)
+}
+
+module.exports = self
diff --git a/src/client/javascript/scrollSnap.js b/src/client/javascript/scrollSnap.js
new file mode 100644
index 0000000..afee979
--- /dev/null
+++ b/src/client/javascript/scrollSnap.js
@@ -0,0 +1,59 @@
+require('smoothscroll-polyfill').polyfill()
+
+const self = {}
+const schedule = require('./schedule')
+
+self._nodes = {
+ search: document.querySelector('#search'),
+ weekSelector: document.querySelector('#week-selector')
+}
+
+self._timeoutID = null
+
+self._getScrollPosition = function () {
+ return (document.documentElement && document.documentElement.scrollTop) ||
+ document.body.scrollTop
+}
+
+self._handleDoneScrolling = function () {
+ const scrollPosition = self._getScrollPosition()
+ const weekSelectorHeight =
+ self._nodes.weekSelector.clientHeight - self._nodes.search.clientHeight
+ if (scrollPosition < weekSelectorHeight && scrollPosition > 0) {
+ window.scroll({ top: weekSelectorHeight, left: 0, behavior: 'smooth' })
+ }
+}
+
+self._handleScroll = function () {
+ if (self._timeoutID != null) window.clearTimeout(self._timeoutID)
+ self._timeoutID = window.setTimeout(self._handleDoneScrolling, 500)
+
+ const scrollPosition = self._getScrollPosition()
+ const weekSelectorHeight =
+ self._nodes.weekSelector.clientHeight - self._nodes.search.clientHeight
+ if (scrollPosition >= weekSelectorHeight) {
+ document.body.classList.add('week-selector-not-visible')
+ } else {
+ document.body.classList.remove('week-selector-not-visible')
+ }
+}
+
+self._handleWindowResize = function () {
+ const weekSelectorHeight =
+ self._nodes.weekSelector.clientHeight - self._nodes.search.clientHeight
+ const extraPixelsNeeded =
+ weekSelectorHeight - (document.body.clientHeight - window.innerHeight)
+ if (extraPixelsNeeded > 0) {
+ document.body.style.marginBottom = extraPixelsNeeded + 'px'
+ } else {
+ document.body.style.marginBottom = null
+ }
+}
+
+self.startListening = function () {
+ window.addEventListener('scroll', self._handleScroll)
+}
+
+schedule.on('load', self._handleWindowResize)
+window.addEventListener('resize', self._handleWindowResize)
+module.exports = self
diff --git a/src/client/javascript/search.js b/src/client/javascript/search.js
new file mode 100644
index 0000000..96413b0
--- /dev/null
+++ b/src/client/javascript/search.js
@@ -0,0 +1,95 @@
+/* global USERS */
+
+const EventEmitter = require('events')
+const fuzzy = require('fuzzy')
+const autocomplete = require('./autocomplete')
+const browserFixToolkit = require('./browserFixToolkit')
+
+const self = new EventEmitter()
+
+self._nodes = {
+ search: document.querySelector('#search'),
+ input: document.querySelector('input[type="search"]')
+}
+
+self.submit = function () {
+ const selectedUser = autocomplete.getSelectedUser()
+ if (selectedUser == null) return
+
+ console.log(selectedUser)
+
+ self._nodes.input.blur()
+ document.body.classList.remove('week-selector-not-visible') // Safari bug
+
+ self.emit('search', selectedUser)
+}
+
+self.updateDom = function (selectedUser) {
+ if (selectedUser == null) {
+ self._nodes.input.value = ''
+ autocomplete.removeAllItems()
+ document.body.classList.add('no-input')
+ document.body.classList.remove('searched')
+ } else {
+ self._nodes.input.value = selectedUser.value
+ autocomplete.removeAllItems()
+ document.body.classList.remove('no-input')
+ document.body.classList.add('searched')
+ }
+}
+
+self.focus = function () {
+ self._nodes.input.focus()
+}
+
+self._handleSubmit = function (event) {
+ event.preventDefault()
+ self.submit()
+}
+
+self._calculate = function (searchTerm) {
+ const allResults = fuzzy.filter(searchTerm, USERS, {
+ extract: function (user) { return user.value }
+ })
+ const firstResults = allResults.slice(0, 7)
+
+ const originalResults = firstResults.map(function (result) {
+ return result.original
+ })
+
+ return originalResults
+}
+
+self._handleTextUpdate = function () {
+ const results = self._calculate(self._nodes.input.value)
+
+ autocomplete.removeAllItems()
+ for (let i = 0; i < results.length; i++) {
+ autocomplete.addItem(results[i])
+ }
+}
+
+self._handleFocus = function () {
+ self._nodes.input.select()
+}
+
+self._handleBlur = function () {
+ // this will removed the selection without drawing focus on it (safari)
+ // this will removed selection even when focusing an iframe (chrome)
+ const oldValue = self._nodes.value
+ self._nodes.value = ''
+ self._nodes.value = oldValue
+
+ // this will hide the keyboard (iOS safari)
+ document.activeElement.blur()
+}
+
+autocomplete.on('select', self.submit)
+
+self._nodes.search.addEventListener('submit', self._handleSubmit)
+self._nodes.input.addEventListener('focus', self._handleFocus)
+self._nodes.input.addEventListener('blur', self._handleBlur)
+self._nodes.input.addEventListener(browserFixToolkit.inputEvent,
+ self._handleTextUpdate)
+
+module.exports = self
diff --git a/src/client/javascript/url.js b/src/client/javascript/url.js
new file mode 100644
index 0000000..17ab7c8
--- /dev/null
+++ b/src/client/javascript/url.js
@@ -0,0 +1,67 @@
+/* global USERS FLAGS */
+
+const EventEmitter = require('events')
+
+const self = new EventEmitter()
+
+self._getPageTitle = function (selectedUser) {
+ let ret
+
+ if (selectedUser == null) {
+ ret = `Metis Rooster`
+ } else {
+ ret = `Metis Rooster - ${selectedUser.value}`
+ }
+
+ if (FLAGS.indexOf('BETA') !== -1) {
+ ret = `BETA ${ret}`
+ }
+
+ return ret
+}
+
+self._getPageURL = function (selectedUser) {
+ return `/${selectedUser.type}/${selectedUser.value}`
+}
+
+self.push = function (selectedUser, push) {
+ if (push == null) push = true
+ const pageTitle = self._getPageTitle(selectedUser)
+ const pageURL = self._getPageURL(selectedUser)
+ if (push) {
+ window.history.pushState(selectedUser, pageTitle, pageURL)
+ } else {
+ window.history.replaceState(selectedUser, pageTitle, pageURL)
+ }
+}
+
+self.update = function (selectedUser) {
+ document.title = self._getPageTitle(selectedUser)
+}
+
+self.hasSelectedUser = function () {
+ const pageUrl = window.location.pathname
+ return /^\/s\/|^\/t\/|^\/r\/|^\/c\//.test(pageUrl)
+}
+
+self.getSelectedUser = function () {
+ const pageUrl = window.location.pathname
+ const pageUrlData = pageUrl.split('/')
+ const type = pageUrlData[1]
+ const value = pageUrlData[2]
+
+ const user = USERS.filter(function (user) {
+ return user.type === type &&
+ user.value === value
+ })[0]
+
+ return user
+}
+
+self._handleUpdate = function (event) {
+ self.emit('update', event.state)
+}
+
+window.addEventListener('popstate', self._handleUpdate)
+
+module.exports = self
diff --git a/src/client/javascript/weekSelector.js b/src/client/javascript/weekSelector.js
new file mode 100644
index 0000000..d4e7f2a
--- /dev/null
+++ b/src/client/javascript/weekSelector.js
@@ -0,0 +1,99 @@
+const EventEmitter = require('events')
+
+const self = new EventEmitter()
+
+self._nodes = {
+ prevButton: document.querySelectorAll('#week-selector button')[0],
+ nextButton: document.querySelectorAll('#week-selector button')[1],
+ currentWeekNode: document.querySelector('#week-selector .current'),
+ currentWeekNormalText: document.querySelector('#week-selector .current .no-print'),
+ currentWeekPrintText: document.querySelector('#week-selector .current .print')
+}
+
+self._weekOffset = 0
+
+// copied from http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/untisscripts.js,
+// were using the same code as they do to be sure that we always get the same
+// week number.
+self.getCurrentWeek = function (target) {
+ const dayNr = (target.getDay() + 6) % 7
+ target.setDate(target.getDate() - dayNr + 3)
+ const firstThursday = target.valueOf()
+ target.setMonth(0, 1)
+ if (target.getDay() !== 4) {
+ target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7)
+ }
+
+ return 1 + Math.ceil((firstThursday - target) / 604800000)
+}
+
+self.getSelectedWeek = function () {
+ const now = new Date()
+ const targetDate = new Date(now.getTime() +
+ self._weekOffset * 604800 * 1000 + 86400 * 1000)
+ return self.getCurrentWeek(targetDate)
+}
+
+self.updateCurrentWeek = function () {
+ const selectedWeekNumber = self.getSelectedWeek()
+ if (self.getCurrentWeek(new Date()) !== selectedWeekNumber) {
+ self._nodes.currentWeekNode.classList.add('changed')
+ } else {
+ self._nodes.currentWeekNode.classList.remove('changed')
+ }
+ self.updateDom()
+ self.emit('weekChanged', selectedWeekNumber)
+}
+
+self.updateDom = function () {
+ const selectedWeekNumber = self.getSelectedWeek()
+ const isSunday = new Date().getDay() === 0
+ let humanReadableWeek = null
+ if (isSunday) {
+ switch (self._weekOffset) {
+ case 0:
+ humanReadableWeek = 'Aanstaande week'
+ break
+ case 1:
+ humanReadableWeek = 'Volgende week'
+ break
+ case -1:
+ humanReadableWeek = 'Afgelopen week'
+ break
+ }
+ } else {
+ switch (self._weekOffset) {
+ case 0:
+ humanReadableWeek = 'Huidige week'
+ break
+ case 1:
+ humanReadableWeek = 'Volgende week'
+ break
+ case -1:
+ humanReadableWeek = 'Vorige week'
+ break
+ }
+ }
+ if (humanReadableWeek != null) {
+ self._nodes.currentWeekNormalText.textContent = humanReadableWeek + ' • ' + selectedWeekNumber
+ self._nodes.currentWeekPrintText.textContent = 'Week ' + selectedWeekNumber
+ } else {
+ self._nodes.currentWeekNormalText.textContent = 'Week ' + selectedWeekNumber
+ self._nodes.currentWeekPrintText.textContent = 'Week ' + selectedWeekNumber
+ }
+}
+
+self._handlePrevButtonClick = function () {
+ self._weekOffset -= 1
+ self.updateCurrentWeek()
+}
+
+self._handleNextButtonClick = function () {
+ self._weekOffset += 1
+ self.updateCurrentWeek()
+}
+
+self._nodes.prevButton.addEventListener('click', self._handlePrevButtonClick)
+self._nodes.nextButton.addEventListener('click', self._handleNextButtonClick)
+
+module.exports = self
diff --git a/src/client/javascript/zoom.js b/src/client/javascript/zoom.js
new file mode 100644
index 0000000..59b80db
--- /dev/null
+++ b/src/client/javascript/zoom.js
@@ -0,0 +1,30 @@
+const schedule = require('./schedule')
+
+const self = {}
+
+self._nodes = {
+ body: document.body
+}
+
+self._handleResize = function () {
+ // the table node may not exist before this function is called
+ const tableNode = document.querySelector('center > table')
+
+ // infact, it may not even exist when this function is called.
+ if (!tableNode) return
+
+ const tableWidth = tableNode.getBoundingClientRect().width
+ const tableGoalWidth = self._nodes.body.getBoundingClientRect().width * 0.9
+ const zoomFactor = tableGoalWidth / tableWidth
+
+ if (zoomFactor < 1) {
+ tableNode.style.zoom = `${zoomFactor}`
+ } else {
+ tableNode.style.zoom = `1`
+ }
+}
+
+schedule.on('load', self._handleResize)
+window.addEventListener('resize', self._handleResize)
+
+module.exports = self