diff options
| author | Noah Loomans <noahloomans@gmail.com> | 2017-09-13 16:28:53 +0200 | 
|---|---|---|
| committer | Noah Loomans <noahloomans@gmail.com> | 2017-09-13 16:28:53 +0200 | 
| commit | 3fb86482404e11942cd83c3500a297a3991db0e4 (patch) | |
| tree | c5e7261de72c2b7f871580784525d06d036a6219 /src | |
| parent | 5aac32f72eca8c66e879583ce653d07bb3c7370f (diff) | |
Restructure project
Diffstat (limited to 'src')
47 files changed, 1981 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 = '' +} + +self.delete = function () { +  window.localStorage.removeItem('fav') +} + +self.updateDom = function (isFavorite) { +  if (isFavorite) { +    self._nodes.toggle.innerHTML = '' +  } else { +    self._nodes.toggle.innerHTML = '' +  } +} + +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..38ad66d --- /dev/null +++ b/src/client/javascript/schedule.js @@ -0,0 +1,78 @@ +/* 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() +    search.updateDom(selectedUser) +  } else if (VALID_WEEK_NUMBERS.indexOf(week) === -1) { +    self._handleError({ target: { status: 404 } }); +    return +  } 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 diff --git a/src/client/static/.well-known/keybase.txt b/src/client/static/.well-known/keybase.txt new file mode 100644 index 0000000..7e11526 --- /dev/null +++ b/src/client/static/.well-known/keybase.txt @@ -0,0 +1,54 @@ +================================================================== +https://keybase.io/nloomans +-------------------------------------------------------------------- + +I hereby claim: + +  * I am an admin of https://rooster.hetmml.nl +  * I am nloomans (https://keybase.io/nloomans) on keybase. +  * I have a public key ASCCV4aRFiMkEv7inJTf34RgxZ6IK0wQ-wTH2ZfSIu3OzAo + +To do so, I am signing this object: + +{ +    "body": { +        "key": { +            "eldest_kid": "0101bbdb28841b169de6538a51d17ca94b30088ba2914e56fd19121eec05f7a389cc0a", +            "host": "keybase.io", +            "kid": "01208257869116232412fee29c94dfdf8460c59e882b4c10fb04c7d997d222edcecc0a", +            "uid": "7a52ddabf92293dd59f8fbf3774ea319", +            "username": "nloomans" +        }, +        "service": { +            "hostname": "rooster.hetmml.nl", +            "protocol": "https:" +        }, +        "type": "web_service_binding", +        "version": 1 +    }, +    "client": { +        "name": "keybase.io go client", +        "version": "1.0.20" +    }, +    "ctime": 1492017398, +    "expire_in": 504576000, +    "merkle_root": { +        "ctime": 1492017367, +        "hash": "463e597079ce3829ccc1f1aa7b15533c0848f9e13cdb55407af490a87bf4ac1b2d64e8235518ada07d93003b889157b576aad02eda294ccd594dc0dcbf8862ef", +        "seqno": 1015311 +    }, +    "prev": "36959cd282a98f651138068f8695b07480a016f02ba99a0acbde277e0cf4ca30", +    "seqno": 19, +    "tag": "signature" +} + +which yields the signature: + +hKRib2R5hqhkZXRhY2hlZMOpaGFzaF90eXBlCqNrZXnEIwEggleGkRYjJBL+4pyU39+EYMWeiCtMEPsEx9mX0iLtzswKp3BheWxvYWTFAvt7ImJvZHkiOnsia2V5Ijp7ImVsZGVzdF9raWQiOiIwMTAxYmJkYjI4ODQxYjE2OWRlNjUzOGE1MWQxN2NhOTRiMzAwODhiYTI5MTRlNTZmZDE5MTIxZWVjMDVmN2EzODljYzBhIiwiaG9zdCI6ImtleWJhc2UuaW8iLCJraWQiOiIwMTIwODI1Nzg2OTExNjIzMjQxMmZlZTI5Yzk0ZGZkZjg0NjBjNTllODgyYjRjMTBmYjA0YzdkOTk3ZDIyMmVkY2VjYzBhIiwidWlkIjoiN2E1MmRkYWJmOTIyOTNkZDU5ZjhmYmYzNzc0ZWEzMTkiLCJ1c2VybmFtZSI6Im5sb29tYW5zIn0sInNlcnZpY2UiOnsiaG9zdG5hbWUiOiJyb29zdGVyLmhldG1tbC5ubCIsInByb3RvY29sIjoiaHR0cHM6In0sInR5cGUiOiJ3ZWJfc2VydmljZV9iaW5kaW5nIiwidmVyc2lvbiI6MX0sImNsaWVudCI6eyJuYW1lIjoia2V5YmFzZS5pbyBnbyBjbGllbnQiLCJ2ZXJzaW9uIjoiMS4wLjIwIn0sImN0aW1lIjoxNDkyMDE3Mzk4LCJleHBpcmVfaW4iOjUwNDU3NjAwMCwibWVya2xlX3Jvb3QiOnsiY3RpbWUiOjE0OTIwMTczNjcsImhhc2giOiI0NjNlNTk3MDc5Y2UzODI5Y2NjMWYxYWE3YjE1NTMzYzA4NDhmOWUxM2NkYjU1NDA3YWY0OTBhODdiZjRhYzFiMmQ2NGU4MjM1NTE4YWRhMDdkOTMwMDNiODg5MTU3YjU3NmFhZDAyZWRhMjk0Y2NkNTk0ZGMwZGNiZjg4NjJlZiIsInNlcW5vIjoxMDE1MzExfSwicHJldiI6IjM2OTU5Y2QyODJhOThmNjUxMTM4MDY4Zjg2OTViMDc0ODBhMDE2ZjAyYmE5OWEwYWNiZGUyNzdlMGNmNGNhMzAiLCJzZXFubyI6MTksInRhZyI6InNpZ25hdHVyZSJ9o3NpZ8RAcP5FuvbGM9nXBzWqChr9zdj452IpBzrVbd6YvcktLyKjaUaRg51BOWsyHmYQ+uxmZ2ZCUI6xZbbJ1SIAnWqvC6hzaWdfdHlwZSCkaGFzaIKkdHlwZQildmFsdWXEING7Z+BlY2sOTnQqQJo/PUBashy75VL9UU4tGIEvMXbco3RhZ80CAqd2ZXJzaW9uAQ== + +And finally, I am proving ownership of this host by posting or +appending to this document. + +View my publicly-auditable identity here: https://keybase.io/nloomans + +================================================================== diff --git a/src/client/static/apple-touch-icon.png b/src/client/static/apple-touch-icon.pngBinary files differ new file mode 100644 index 0000000..5adfc69 --- /dev/null +++ b/src/client/static/apple-touch-icon.png diff --git a/src/client/static/browserconfig.xml b/src/client/static/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/src/client/static/browserconfig.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<browserconfig> +    <msapplication> +        <tile> +            <square150x150logo src="/mstile-150x150.png"/> +            <TileColor>#da532c</TileColor> +        </tile> +    </msapplication> +</browserconfig> diff --git a/src/client/static/favicon-16x16.png b/src/client/static/favicon-16x16.pngBinary files differ new file mode 100644 index 0000000..1df47d3 --- /dev/null +++ b/src/client/static/favicon-16x16.png diff --git a/src/client/static/favicon-32x32.png b/src/client/static/favicon-32x32.pngBinary files differ new file mode 100644 index 0000000..36cd5da --- /dev/null +++ b/src/client/static/favicon-32x32.png diff --git a/src/client/static/favicon.ico b/src/client/static/favicon.icoBinary files differ new file mode 100644 index 0000000..c201043 --- /dev/null +++ b/src/client/static/favicon.ico diff --git a/src/client/static/icons/mml-logo.png b/src/client/static/icons/mml-logo.pngBinary files differ new file mode 100644 index 0000000..fa5ae11 --- /dev/null +++ b/src/client/static/icons/mml-logo.png diff --git a/src/client/static/icons/res/mipmap-hdpi/ic_launcher.png b/src/client/static/icons/res/mipmap-hdpi/ic_launcher.pngBinary files differ new file mode 100644 index 0000000..514ad14 --- /dev/null +++ b/src/client/static/icons/res/mipmap-hdpi/ic_launcher.png diff --git a/src/client/static/icons/res/mipmap-mdpi/ic_launcher.png b/src/client/static/icons/res/mipmap-mdpi/ic_launcher.pngBinary files differ new file mode 100644 index 0000000..59bab1d --- /dev/null +++ b/src/client/static/icons/res/mipmap-mdpi/ic_launcher.png diff --git a/src/client/static/icons/res/mipmap-xhdpi/ic_launcher.png b/src/client/static/icons/res/mipmap-xhdpi/ic_launcher.pngBinary files differ new file mode 100644 index 0000000..908a6e8 --- /dev/null +++ b/src/client/static/icons/res/mipmap-xhdpi/ic_launcher.png diff --git a/src/client/static/icons/res/mipmap-xxhdpi/ic_launcher.png b/src/client/static/icons/res/mipmap-xxhdpi/ic_launcher.pngBinary files differ new file mode 100644 index 0000000..f12048f --- /dev/null +++ b/src/client/static/icons/res/mipmap-xxhdpi/ic_launcher.png diff --git a/src/client/static/icons/res/mipmap-xxxhdpi/ic_launcher.png b/src/client/static/icons/res/mipmap-xxxhdpi/ic_launcher.pngBinary files differ new file mode 100644 index 0000000..7a0462e --- /dev/null +++ b/src/client/static/icons/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/client/static/manifest.beta.webmanifest b/src/client/static/manifest.beta.webmanifest new file mode 100644 index 0000000..a1fdd92 --- /dev/null +++ b/src/client/static/manifest.beta.webmanifest @@ -0,0 +1,29 @@ +{ +  "name": "BETA Metis Rooster", +  "short_name": "BETA Rooster", +  "start_url": "/", +  "display": "standalone", +  "background_color": "#ececec", +  "description": "Een verbeterde rooster pagina voor het metis", +  "icons": [{ +    "src": "/icons/res/mipmap-mdpi/ic_launcher.png", +    "sizes": "48x48", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-hdpi/ic_launcher.png", +    "sizes": "72x72", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-xhdpi/ic_launcher.png", +    "sizes": "96x96", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-xxhdpi/ic_launcher.png", +    "sizes": "144x144", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-xxxhdpi/ic_launcher.png", +    "sizes": "192x192", +    "type": "image/png" +  }] +} diff --git a/src/client/static/manifest.webmanifest b/src/client/static/manifest.webmanifest new file mode 100644 index 0000000..d33ee8e --- /dev/null +++ b/src/client/static/manifest.webmanifest @@ -0,0 +1,29 @@ +{ +  "name": "Metis Rooster", +  "short_name": "Rooster", +  "start_url": "/", +  "display": "standalone", +  "background_color": "#ececec", +  "description": "Een verbeterde rooster pagina voor het metis", +  "icons": [{ +    "src": "/icons/res/mipmap-mdpi/ic_launcher.png", +    "sizes": "48x48", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-hdpi/ic_launcher.png", +    "sizes": "72x72", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-xhdpi/ic_launcher.png", +    "sizes": "96x96", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-xxhdpi/ic_launcher.png", +    "sizes": "144x144", +    "type": "image/png" +  }, { +    "src": "/icons/res/mipmap-xxxhdpi/ic_launcher.png", +    "sizes": "192x192", +    "type": "image/png" +  }] +} diff --git a/src/client/static/mstile-150x150.png b/src/client/static/mstile-150x150.pngBinary files differ new file mode 100644 index 0000000..5e381e6 --- /dev/null +++ b/src/client/static/mstile-150x150.png diff --git a/src/client/static/safari-pinned-tab.svg b/src/client/static/safari-pinned-tab.svg new file mode 100644 index 0000000..97ce8bf --- /dev/null +++ b/src/client/static/safari-pinned-tab.svg @@ -0,0 +1,34 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" + width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000" + preserveAspectRatio="xMidYMid meet"> +<metadata> +Created by potrace 1.11, written by Peter Selinger 2001-2013 +</metadata> +<g transform="translate(0.000000,16.000000) scale(0.012030,-0.012030)" +fill="#000000" stroke="none"> +<path d="M498 1233 c-10 -20 -1 -650 11 -690 18 -64 84 -154 141 -192 82 -55 +144 -66 385 -66 l209 0 1 478 c0 262 0 477 0 477 -1 0 -20 -18 -44 -40 -24 +-22 -64 -52 -89 -67 -44 -26 -49 -27 -231 -28 -102 -1 -202 3 -221 8 -47 12 +-107 54 -135 95 -13 18 -25 30 -27 25z"/> +<path d="M405 917 c-95 -24 -193 -97 -250 -187 -77 -122 -83 -287 -14 -421 30 +-60 113 -142 173 -173 171 -86 353 -62 489 64 48 45 50 49 27 50 -14 0 -41 +-13 -59 -30 -33 -27 -131 -86 -131 -77 0 2 14 32 30 66 17 35 30 64 28 65 -42 +30 -86 48 -120 49 -42 2 -42 2 -43 42 -1 31 -27 95 -39 95 -1 0 -2 -30 -1 -67 +l0 -68 -73 -3 c-84 -3 -84 -2 -97 105 l-7 62 77 -1 c42 0 78 2 81 5 2 2 2 11 +-2 20 -4 12 -18 16 -48 16 -22 -1 -56 -1 -74 0 l-34 1 6 53 c4 28 10 66 14 82 +8 29 11 30 62 30 43 0 54 3 56 18 2 14 -6 17 -41 17 -24 0 -46 3 -48 8 -7 10 +39 102 65 129 22 24 32 50 20 56 -4 2 -25 -1 -47 -6z m-58 -119 l-32 -68 -52 +0 c-29 0 -53 4 -53 8 0 15 74 80 120 106 24 14 45 24 46 23 1 -1 -12 -32 -29 +-69z m-52 -153 c-4 -27 -8 -62 -10 -76 -1 -15 -5 -31 -8 -35 -5 -9 -133 -11 +-142 -2 -7 7 15 108 30 138 12 23 19 25 75 25 l62 1 -7 -51z m-14 -168 c0 -7 +2 -19 3 -27 2 -8 6 -41 10 -72 l7 -58 -61 0 -61 0 -19 43 c-10 23 -21 61 -25 +84 l-6 41 76 1 c52 0 75 -3 76 -12z m65 -257 c18 -35 32 -65 31 -67 -14 -13 +-167 102 -167 126 0 4 23 7 52 7 l51 -1 33 -65z m145 58 c3 -5 5 -41 4 -81 -1 +-70 -2 -72 -26 -69 -13 1 -32 13 -42 25 -14 19 -67 119 -67 129 0 7 126 3 131 +-4z m164 0 c8 -7 -35 -94 -61 -125 -11 -12 -28 -23 -39 -25 -18 -3 -20 3 -20 +72 -1 41 0 77 1 80 4 8 111 6 119 -2z"/> +</g> +</svg> diff --git a/src/client/static/stylesheets/hello.css b/src/client/static/stylesheets/hello.css new file mode 100644 index 0000000..edcbc92 --- /dev/null +++ b/src/client/static/stylesheets/hello.css @@ -0,0 +1,23 @@ +* { +  box-sizing: border-box; +} + +html, body { +  margin: 0; +  font-family: 'Roboto', sans-serif; +  display: flex; +  flex-direction: column; +  justify-content: center; +  text-align: center; +  width: 100vw; +  height: 100vh; +  color: gray; +} + +.ideas { +  font-size: 0.8em; +} + +a { +  color: #3f51b5; +} diff --git a/src/client/static/stylesheets/print.css b/src/client/static/stylesheets/print.css new file mode 100644 index 0000000..0e09533 --- /dev/null +++ b/src/client/static/stylesheets/print.css @@ -0,0 +1,63 @@ +#search, #week-selector { +  background-color: inherit; +  box-shadow: inherit; +} + +#search { +  border-bottom: 1px solid black; +  position: absolute; +} + +#search .top-bar, #week-selector .week-wrapper { +  max-width: inherit; +} + +#search input[type="search"] { +  background-color: inherit; +  color: black; +  font-weight: bold; +} + +#search .fav { +  display: none !important; +} + +#search #overflow-button { +  display: none; +} + +#week-selector .week-wrapper { +  display: block; +} + +#week-selector button { +  display: none; +} + +#week-selector .current { +  color: black; +  padding: 16px; +  font-size: 1.1em; +  float: right; +} + +#search-space-filler { +  display: none; +} + +.mdl-menu__container { +  display: none !important; +} + +#schedule { +  padding-top: 16px; +  width: 100%; +} + +.no-print { +  display: none; +} + +.print { +  display: initial; +} diff --git a/src/client/static/stylesheets/style.css b/src/client/static/stylesheets/style.css new file mode 100644 index 0000000..830b007 --- /dev/null +++ b/src/client/static/stylesheets/style.css @@ -0,0 +1,392 @@ +* { +  box-sizing: border-box; +} + +html, body { +  margin: 0; +  font-family: 'Roboto', sans-serif; +} + +.other { +  color: gray; +  font-style: italic; +  margin-left: 5px; +} + +#search { +  z-index: 2; +  background-color: #F44336; +  margin: 0 auto; +  width: 100%; +  position: fixed; +  box-shadow: 0 0.5px 1.5px rgba(0,0,0,0.06), 0 0.5px 1px rgba(0,0,0,0.12); +} + +#search .top-bar { +  position: relative; +  margin: 0 auto; +  max-width: 600px; +  padding: 10px; +  display: flex; +} + +#search .input-wrapper { +  position: relative; +  flex-grow: 1; +  color: #FFFFFF; +} + +#search input[type='search'] { +  display: block; +  background-color: #f6695e; +  color: inherit; +  border-radius: 2px; +  width: 100%; +  display: block; +  outline: none; +  border: 0; +  padding: 16px; +  font-size: 16px; +  transition: box-shadow 200ms ease-in-out; +} + +#search input[type='search']:focus { +  background-color: #FFFFFF; +  color: #212121; +  box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} + +#search input[type='search']:focus + button { +  color: #212121; +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { +  display: none; +} + +input[type="search"]::-ms-clear { +  width: 0; +  height: 0; +} + +button::-moz-focus-inner { +  border: 0; +} + + +/* WebKit, Blink, Edge */ +input::-webkit-input-placeholder { +  color: #FFCDD2; +} +input:focus::-webkit-input-placeholder { +  color: #757575; +} + +/* Mozilla Firefox 4 to 18 */ +input:-moz-placeholder { + color: #FFCDD2; + opacity: 1; +} +input:focus:-moz-placeholder { + color: #757575; +} + +/* Mozilla Firefox 19+ */ +input::-moz-placeholder { + color: #FFCDD2; + opacity: 1; +} +input:focus::-moz-placeholder { + color: #757575; +} + +/* Internet Explorer 10-11 */ +input:-ms-input-placeholder { + color: #FFCDD2; +} +input:focus:-ms-input-placeholder { + color: #757575; +} + +li:hover { +  background-color: lightgray; +  cursor: pointer; +} + +.selected { +  background-color: lightgray; +} + +#schedule { +  overflow: auto; +} + +body.searched #search-space-filler { +  height: 70px; +} + +.autocomplete-wrapper { +  background-color: white; +} + +.autocomplete { +  max-width: 600px; +  margin: 0 auto; +  padding: 0; +} + +.autocomplete li { +  list-style: none; +  padding: 10px; +} + +#week-selector { +  z-index: 1; +  background-color: #F44336; +  box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); +  color: white; +} + +#week-selector .week-wrapper { +  max-width: 600px; +  padding: 10px !important; +  margin: 0 auto; +  display: flex; +  -js-display: flex; +  padding: 10px 0; +} + +#week-selector .current { +  display: flex; +  flex-grow: 1; +  align-items: center; +  justify-content: center; +} + +#week-selector .current.changed { +  font-weight: bold; +} + +#week-selector button { +  background: transparent; +  color: white; +  border: 0px; +  padding: 5px 10px; +  border-radius: 2px; +} + +input { +  -webkit-appearance: none; +} + +#search .fav { +  position: absolute; +  font-size: 1.8em; +  color: inherit; +  right: 8.5px; +  top: 8.5px; +  border: 0; +  padding: 4px; +  border-radius: 2px; +  background: none; + +  display: none; +} + +body.searched #search .fav { +  display: block; +} + +#week-selector button:focus, #search #overflow-button:focus, #search .fav:focus { +  outline: none; +  background-color: #D32F2F; +} + +#search #overflow-button { +  background: none; +  border: none; +  padding: 3px 9px; +  color: white; +  border-radius: 2px; +} + +.hidden { +  display: none !important; +} + +ul a { +  color: inherit; +  text-decoration: none; +} + +#search .title { +  display: none; +} + +body:not(.no-input) { +  overflow-y: scroll; +} + +body.no-input #week-selector { +  display: none; +} + +@media screen and (min-height: 400px) { +  body.no-input { +    background-color: #ececec; +  } + +  body.no-input #search { +    height: 100%; +    background-color: #ececec; +    box-shadow: none; +  } + +  body.no-input #search button { +    display: none; +  } + +  body.no-input #search #overflow-button { +    position: absolute; +    display: block; +    top: 0; +    right: 0; +    color: #757575; +  } + +  body.no-input #search .print-page { +    display: none; +  } + +  body.no-input #search #overflow-button:focus { +    background-color: inherit; +    color: #212121; +  } + +  body.no-input #search .logo { +    background-image: url(/icons/mml-logo.png); +    background-position: center; +    background-repeat: no-repeat; +    background-size: contain; +    height: 100px; +    width: 100px; + +    /* virtual center: http://javier.xyz/visual-center/ */ +    transform: translate(-8%,-3%); +    margin: 0 auto; +  } + +  body.no-input #search .title { +    display: block; +    font-size: 55px; +    padding-bottom: 32px; +  } + +  body.no-input #search .title .text { +    text-align: center; +    line-height: 55px; +  } + +  body.no-input #search .top-bar { +    position: static; +    display: block; +    margin-top: 50vh; +    transform: translateY(-75%); +  } + +  body.no-input #search input[type='search'] { +    background-color: #FFF; +  } + +  /* WebKit, Blink, Edge */ +  body.no-input #search input::-webkit-input-placeholder { +    color: #757575; +  } + +  /* Mozilla Firefox 4 to 18 */ +  body.no-input #search input:-moz-placeholder { +    color: #757575; +    opacity: 1; +  } + +  /* Mozilla Firefox 19+ */ +  body.no-input #search input::-moz-placeholder { +    color: #757575; +    opacity: 1; +  } + +  /* Internet Explorer 10-11 */ +  body.no-input #search input:-ms-input-placeholder { +    color: #757575; +  } + +  body.no-input .tooltip { +    display: block; +    position: absolute; +    background-color: white; +    padding: 15px; +    margin: 32px 8px; +    border-radius: 2px; + +    left: 16px; +    right: 16px; +  } + +  body.no-input .tooltip::before { +    content: ''; +    width: 24px; +    height: 24px; +    background-color: white; +    top: -12px; +    position: absolute; +    transform: rotate(45deg); +    z-index: -1; +  } +} + +.tooltip { +  display: none; +} + +.error { +  text-align: center; +  margin-top: 100px; +  padding: 16px; +} + +body.week-selector-not-visible #search { +  box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); +} + +body.week-selector-not-visible #week-selector { +  box-shadow: inherit; +} + +.print { +  display: none; +} + +#notification { +  max-width: 600px; +  padding: 10px; +  margin: 0 auto; +} + +#notification .box { +  display: flex; +  background-color: #e0e0e0; +  padding: 8px; +  border-radius: 2px; +  align-items: center; +} + +#notification .text { +  padding-left: 8px; +} + +.grow { +  flex-grow: 1; +} diff --git a/src/client/static/sw.js b/src/client/static/sw.js new file mode 100644 index 0000000..bd43805 --- /dev/null +++ b/src/client/static/sw.js @@ -0,0 +1,29 @@ +/* global importScripts toolbox self */ + +(global => { +  'use strict' + +  // Load the sw-toolbox library. +  importScripts('/components/sw-toolbox/sw-toolbox.js') + +  // Ensure that our service worker takes control of the page as soon as possible. +  global.addEventListener('install', event => event.waitUntil(global.skipWaiting())) +  global.addEventListener('activate', event => event.waitUntil(global.clients.claim())) + +  toolbox.precache([ +    '/', +    '/hello', +    '/untisinfo.css', +    '/javascripts/bundle.js', +    '/stylesheets/style.css', +    '/stylesheets/hello.css' +  ]) + +  toolbox.router.get('/', toolbox.fastest) +  toolbox.router.get('/hello', toolbox.fastest) + +  toolbox.router.get('/javascripts/bundle.js', toolbox.fastest) +  toolbox.router.get('/stylesheets/*', toolbox.fastest) +  toolbox.router.get('/untisinfo.css', toolbox.fastest) +  toolbox.router.get('/meetingpointProxy/*', toolbox.networkFirst) +})(self) diff --git a/src/client/static/untisinfo.css b/src/client/static/untisinfo.css new file mode 100644 index 0000000..d74a7aa --- /dev/null +++ b/src/client/static/untisinfo.css @@ -0,0 +1,11 @@ +html, body { +  overflow: auto; +  width: 100vw; +  height: 100vh; +  margin: 0; +  -webkit-overflow-scrolling: touch; +} + +center { +  margin: 5px; +} diff --git a/src/client/views/error.jade b/src/client/views/error.jade new file mode 100644 index 0000000..51ec12c --- /dev/null +++ b/src/client/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content +  h1= message +  h2= error.status +  pre #{error.stack} diff --git a/src/client/views/index.jade b/src/client/views/index.jade new file mode 100644 index 0000000..540fd42 --- /dev/null +++ b/src/client/views/index.jade @@ -0,0 +1,51 @@ +extends layout + +block variables +  - var bodyStyle = 'opacity: 0;'; + +block head +  link(rel='stylesheet', href='/stylesheets/style.css') +  link(rel='stylesheet', href='/stylesheets/print.css', media='print') +  link(rel='stylesheet', href='https://fonts.googleapis.com/icon?family=Material+Icons') +  link(rel='stylesheet', href='/components/material-design-lite/material.min.css') +  script(defer='', src='/components/material-design-lite/material.min.js') + +block content +  form#search +    .top-bar +      .title +        .logo +        .text Rooster +      .input-wrapper +        input(type='search', placeholder='Zoeken', autocomplete='off') +        button.material-icons.fav(tabindex='0', type='button')  +      .tooltip +        span Voer hier een <strong>docentafkorting</strong>, <strong>klas</strong>, <strong>leerlingnummer</strong> of <strong>lokaalnummer</strong> in. +      button#overflow-button(type='button') +        i.material-icons  +         +      ul.mdl-menu.mdl-menu--bottom-right.mdl-js-menu.mdl-js-ripple-effect(for='overflow-button') +        a(href='http://www.meetingpointmco.nl/Roosters-AL/doc/basisroosters/default.htm') +          li.mdl-menu__item Basis rooster gebruiken +        a(href='http://www.meetingpointmco.nl/Roosters-AL/doc/') +          li.mdl-menu__item Oud rooster gebruiken +        a(href='javascript:window.print()').print-page +          li.mdl-menu__item.mdl-menu__item--full-bleed-divider#print-page Pagina printen +        li.mdl-menu__item(disabled) Gemaakt door Noah Loomans +    .autocomplete-wrapper +      ul.autocomplete +  #week-selector +    #search-space-filler +    .week-wrapper +      button(type='button').material-icons  +      span.current  +        span.no-print Loading... +        span.print +      button(type='button').material-icons  +   +  #schedule + +block scripts +  script. +    !{flagsStr}!{usersStr}!{validWeekNumbersStr} +  script(src='/bundle.js') diff --git a/src/client/views/layout.jade b/src/client/views/layout.jade new file mode 100644 index 0000000..f7f9e1f --- /dev/null +++ b/src/client/views/layout.jade @@ -0,0 +1,25 @@ +block variables +  - var bodyStyle = ''; + +doctype html +html(lang='nl') +  head +    block head_top +    if isBeta +      title BETA Metis Rooster +    else +      title Metis Rooster +    meta(name='theme-color',content='#F44336') +    meta(name='viewport', content='width=device-width, initial-scale=1') +    link(href='https://fonts.googleapis.com/css?family=Roboto', rel='stylesheet') +    link(rel='manifest', href='/manifest.webmanifest') +    link(rel="apple-touch-icon", sizes="120x120", href="/apple-touch-icon.png") +    link(rel="icon", type="image/png", href="/favicon-32x32.png", sizes="32x32") +    link(rel="icon", type="image/png", href="/favicon-16x16.png", sizes="16x16") +    link(rel="mask-icon", href="/safari-pinned-tab.svg", color="#f44336") +    block head +  body(style=bodyStyle) +    block content +    script. +      (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');ga('create', 'UA-83684754-1', 'auto');ga('send', 'pageview'); +    block scripts diff --git a/src/client/views/redirect.jade b/src/client/views/redirect.jade new file mode 100644 index 0000000..e895456 --- /dev/null +++ b/src/client/views/redirect.jade @@ -0,0 +1,47 @@ +extends layout + +block head +  style. +    body { +      font-family: 'Roboto', sans-serif; +      text-align: center; +      margin: 32px; +    } +     +    .content { +      max-width: 600px; +      margin: 0 auto; +    } +     +    h1 { +      color: #212121; +      margin-bottom: 32px; +    } +     +    h2 { +      color: #727272; +      margin-bottom: 64px; +    } +     +    a, a:visited { +      margin: 8px; +      padding: 8px 16px; +      background-color: #c84127; +      color: white; +      text-decoration: none; +      font-weight: bold; +      border-radius: 3px; +    } +     +    a:hover, a:focus, a:active { +      background-color: #e45a3f; +    } + +block content +  .content +    script document.body.style.opacity = 1 +    img(src='/icons/mml-logo.png') +    h1 Er is iets mis gegaan tijdens het ophalen van de benodigde informatie +    h2 Je kunt proberen door te gaan naar het oude rooster of Magister +    a(href='http://www.meetingpointmco.nl/Roosters-AL/doc/') Oud rooster  +    a(href='http://msa.magister.net/') Magister  diff --git a/src/server/app.js b/src/server/app.js new file mode 100644 index 0000000..36d7e26 --- /dev/null +++ b/src/server/app.js @@ -0,0 +1,62 @@ +const express = require('express') +const path = require('path') +const logger = require('morgan') +const cookieParser = require('cookie-parser') +const bodyParser = require('body-parser') +const compression = require('compression') + +const routes = require('./routes/index') +const getSchedule = require('./routes/getSchedule') +const manifest = require('./routes/manifest') + +const app = express() + +app.use(compression()) + +// view engine setup +app.set('views', path.join(__dirname, '../client/views')) +app.set('view engine', 'jade') + +app.use(logger('dev')) +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: false })) +app.use(cookieParser()) + +app.use('/manifest.webmanifest', manifest) +app.use(express.static(path.join(__dirname, '../client/static'))) + +app.use('/', routes) +app.use('/get', getSchedule) + +// catch 404 and forward to error handler +app.use(function (req, res, next) { +  const err = new Error('Not Found') +  err.status = 404 +  next(err) +}) + +// error handlers + +// development error handler +// will print stacktrace +if (app.get('env') === 'development') { +  app.use(function (err, req, res, next) { +    res.status(err.status || 500) +    res.render('error', { +      message: err.message, +      error: err +    }) +  }) +} + +// production error handler +// no stacktraces leaked to user +app.use(function (err, req, res, next) { +  res.status(err.status || 500) +  res.render('error', { +    message: err.message, +    error: {} +  }) +}) + +module.exports = app diff --git a/src/server/bin/www b/src/server/bin/www new file mode 100755 index 0000000..545db41 --- /dev/null +++ b/src/server/bin/www @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +const app = require('../app') +const http = require('http') + +const port = normalizePort(process.env.PORT || '3000') +const server = http.createServer(app) + +server.listen(port) +server.on('error', error => onError(error, port)) +server.on('listening', _ => onListening(server)) + +function normalizePort (val) { +  const port = parseInt(val, 10) + +  if (isNaN(port)) { +    // named pipe +    return val +  } + +  if (port >= 0) { +    // port number +    return port +  } + +  return false +} + +function onError (error, port) { +  if (error.syscall !== 'listen') { +    throw error +  } + +  const bind = typeof port === 'string' +    ? 'Pipe ' + port +    : 'Port ' + port + +  // handle specific listen errors with friendly messages +  switch (error.code) { +    case 'EACCES': +      console.error(bind + ' requires elevated privileges') +      process.exit(1) +      break +    case 'EADDRINUSE': +      console.error(bind + ' is already in use') +      process.exit(1) +      break +    default: +      throw error +  } +} + +function onListening (server) { +  const addr = server.address() +  if (typeof addr === 'string') { +    console.log(`Listening on pipe ${addr}`) +  } else { +    console.log(`Listening on http://localhost:${addr.port}/`) +  } +} diff --git a/src/server/lib/getMeetingpointData.js b/src/server/lib/getMeetingpointData.js new file mode 100644 index 0000000..94cf36c --- /dev/null +++ b/src/server/lib/getMeetingpointData.js @@ -0,0 +1,83 @@ +'use strict' + +const Promise = require('bluebird') +const cheerio = require('cheerio') +const _ = require('lodash') +const request = Promise.promisify(require('request')) + +let meetingpointData +let lastUpdate + +function getUsers (page) { +  const script = page('script').eq(1).text() + +  const regexs = [/var classes = \[(.+)\];/, /var teachers = \[(.+)\];/, /var rooms = \[(.+)\];/, /var students = \[(.+)\];/] +  const items = regexs.map(function (regex) { +    return script.match(regex)[1].split(',').map(function (item) { +      return item.replace(/"/g, '') +    }) +  }) + +  return [] +  .concat(items[0].map(function (item, index) { +    return { +      type: 'c', +      value: item, +      index: index +    } +  })) +  .concat(items[1].map(function (item, index) { +    return { +      type: 't', +      value: item, +      index: index +    } +  })) +  .concat(items[2].map(function (item, index) { +    return { +      type: 'r', +      value: item, +      index: index +    } +  })) +  .concat(items[3].map(function (item, index) { +    return { +      type: 's', +      value: item, +      index: index +    } +  })) +} + +function getValidWeekNumbers(page) { +  const weekSelector = page('select[name="week"]'); +  const weekNumbers = _.map(weekSelector.children(), option => parseInt(option.attribs.value)) + +  return weekNumbers; +} + +function requestData() { +  lastUpdate = new Date() + +  return request(`http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/frames/navbar.htm`, { timeout: 5000 }).then((response) => { +    const page = cheerio.load(response.body) +    const users = getUsers(page) +    const validWeekNumbers = getValidWeekNumbers(page) + +    meetingpointData = { users, validWeekNumbers } + +    return meetingpointData +  }) +} + +function getMeetingpointData () { +  if (lastUpdate == null || new Date() - lastUpdate > 10 * 60 * 1000) { // 10 minutes +    return requestData() +  } else if (!meetingpointData) { +    return Promise.reject() +  } else { +    return Promise.resolve(meetingpointData) +  } +} + +module.exports = getMeetingpointData diff --git a/src/server/lib/getURLOfUser.js b/src/server/lib/getURLOfUser.js new file mode 100644 index 0000000..2de48e6 --- /dev/null +++ b/src/server/lib/getURLOfUser.js @@ -0,0 +1,8 @@ +const leftPad = require('left-pad') // I imported this just to piss you off ;) + +function getURLOfUser (type, index, week) { +  return `http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/` + +      `${leftPad(week, 2, '0')}/${type}/${type}${leftPad(index + 1, 5, '0')}.htm` +} + +module.exports = getURLOfUser diff --git a/src/server/lib/getUserIndex.js b/src/server/lib/getUserIndex.js new file mode 100644 index 0000000..db7daa8 --- /dev/null +++ b/src/server/lib/getUserIndex.js @@ -0,0 +1,85 @@ +'use strict' + +const Promise = require('bluebird') +const cheerio = require('cheerio') +const request = Promise.promisify(require('request')) + +let userIndex +let lastUpdate + +function updateUserIndex () { +  return new Promise(function (resolve, reject) { +    process.stdout.write('Updating user index... ') +    request(`http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/frames/navbar.htm`) +      .then(function (page) { +        lastUpdate = new Date() +        page = page.body + +        const $ = cheerio.load(page) +        const $script = $('script').eq(1) +        const scriptText = $script.text() + +        const regexs = [/var classes = \[(.+)];/, /var teachers = \[(.+)];/, /var rooms = \[(.+)];/, /var students = \[(.+)];/] +        const items = regexs.map(function (regex) { +          return scriptText.match(regex)[1].split(',').map(function (item) { +            return item.replace(/"/g, '') +          }) +        }) + +        userIndex = ([] +        .concat(items[0].map(function (item, index) { +          return { +            type: 'c', +            value: item, +            index: index +          } +        })) +        .concat(items[1].map(function (item, index) { +          return { +            type: 't', +            value: item, +            index: index +          } +        })) +        .concat(items[2].map(function (item, index) { +          return { +            type: 'r', +            value: item, +            index: index +          } +        })) +        .concat(items[3].map(function (item, index) { +          return { +            type: 's', +            value: item, +            index: index +          } +        }))) + +        process.stdout.write('done.\n') + +        resolve(userIndex) +      }) +      .catch(error => { +        process.stdout.write('failed.\n') +        reject(error) +      }) +  }) +} + +function getUserIndex () { +  return new Promise((resolve, reject) => { +    if (lastUpdate == null) { +      updateUserIndex().then(resolve, reject) +    } else if (new Date() - lastUpdate > 10 * 60 * 1000) { // 10 minutes +      updateUserIndex().then(resolve, function () { +        console.warn('Unable to update userIndex, using cached.') +        resolve(userIndex) +      }) +    } else { +      resolve(userIndex) +    } +  }) +} + +module.exports = getUserIndex diff --git a/src/server/routes/getSchedule.js b/src/server/routes/getSchedule.js new file mode 100644 index 0000000..f6c3cb6 --- /dev/null +++ b/src/server/routes/getSchedule.js @@ -0,0 +1,55 @@ +const express = require('express') +const router = express.Router() +const request = require('request') +const iconv = require('iconv-lite') + +const getUserIndex = require('../lib/getUserIndex') +const getURLOfUser = require('../lib/getURLOfUser') + +// 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. +function getWeekNumber (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) +} + +router.get('/:type/:value', function (req, res, next) { +  getUserIndex().then(users => { +    const { type, value } = req.params +    let { week } = req.query +    const user = +      users.filter(user => user.type === type && user.value === value)[0] + +    if (!user) { +      next(new Error(`${type}${value} is not in the user index.`)) +    } + +    if (!week) { +      week = getWeekNumber(new Date()) +    } + +    const { index } = user + +    const url = getURLOfUser(type, index, week) + +    request(url, { encoding: null }, function (err, data) { +      if (err) { +        next(err) +        return +      } + +      const utf8Body = iconv.decode(data.body, 'ISO-8859-1') +      res.status(data.statusCode).end(utf8Body) +    }) +  }) +}) + +module.exports = router diff --git a/src/server/routes/index.js b/src/server/routes/index.js new file mode 100644 index 0000000..d2267ba --- /dev/null +++ b/src/server/routes/index.js @@ -0,0 +1,31 @@ +'use strict' + +const express = require('express') +const router = express.Router() +const getMeetingpointData = require('../lib/getMeetingpointData') + +/* GET home page. */ +router.get(['/', '/s/*', '/t/*', '/r/*', '/c/*'], function (req, res, next) { +  getMeetingpointData().then(data => { +    const isBeta = process.env.BETA === '1' + +    let flags = [] +    if (isBeta) { +      flags.push('BETA') +      flags.push('NO_FEATURE_DETECT') +    } else if (req.query.nfd != null) { +      flags.push('NO_FEATURE_DETECT') +    } + +    const flagsStr = `var FLAGS = ${JSON.stringify(flags)};` +    const usersStr = `var USERS = ${JSON.stringify(data.users)};` +    const validWeekNumbersStr = `var VALID_WEEK_NUMBERS = ${JSON.stringify(data.validWeekNumbers)}` + +    res.render('index', { flagsStr, usersStr, validWeekNumbersStr }) +  }).catch(function () { +    console.error('Unable to get user info, emergency redirect!') +    res.render('redirect') +  }) +}) + +module.exports = router diff --git a/src/server/routes/manifest.js b/src/server/routes/manifest.js new file mode 100644 index 0000000..b2ce55f --- /dev/null +++ b/src/server/routes/manifest.js @@ -0,0 +1,19 @@ +'use strict' + +const express = require('express') +const router = express.Router() +const path = require('path') + +router.get('/', function (req, res, next) { +  console.log('got a request') + +  const isBeta = process.env.BETA === '1' + +  if (isBeta) { +    res.sendFile('manifest.beta.webmanifest', { root: path.join(__dirname, '../public') }) +  } else { +    res.sendFile('manifest.webmanifest', { root: path.join(__dirname, '../public') }) +  } +}) + +module.exports = router diff --git a/src/server/routes/opensearch.js b/src/server/routes/opensearch.js new file mode 100644 index 0000000..c3e2e57 --- /dev/null +++ b/src/server/routes/opensearch.js @@ -0,0 +1,12 @@ +'use strict' + +const express = require('express') +const router = express.Router() +const path = require('path') + +router.get('/', function (req, res, next) { +  res.setHeader('content-type', 'application/opensearchdescription+xml') +  res.sendFile('opensearch.xml', { root: path.join(__dirname, '../public') }) +}) + +module.exports = router | 
