diff options
Diffstat (limited to 'src')
36 files changed, 495 insertions, 159 deletions
diff --git a/src/client/react/components/container/HelpBox.js b/src/client/react/components/container/HelpBox.js index 31624db..8b53382 100644 --- a/src/client/react/components/container/HelpBox.js +++ b/src/client/react/components/container/HelpBox.js @@ -22,6 +22,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import './HelpBox.scss'; + class HelpBox extends React.Component { static propTypes = { // redux @@ -35,7 +37,7 @@ class HelpBox extends React.Component { } return ( - <div className="help-box"> + <div className="HelpBox"> <div className="arrow" /> <div className="bubble"> Voer hier een docentafkorting, klas, leerlingnummer of lokaalnummer in. diff --git a/src/client/react/components/container/HelpBox.scss b/src/client/react/components/container/HelpBox.scss new file mode 100644 index 0000000..f91eb77 --- /dev/null +++ b/src/client/react/components/container/HelpBox.scss @@ -0,0 +1,23 @@ +.HelpBox { + position: relative; + margin-top: 32px; + + .arrow { + position: absolute; + background-color: white; + width: 32px; + height: 32px; + top: -16px; + left: 48px; + transform: rotate(45deg); + } + + .bubble { + position: relative; + background-color: white; + font-weight: bold; + margin: 16px; + padding: 16px; + border-radius: 4px; + } +} diff --git a/src/client/react/components/container/Menu.js b/src/client/react/components/container/Menu.js index 920973b..0b5c4dc 100644 --- a/src/client/react/components/container/Menu.js +++ b/src/client/react/components/container/Menu.js @@ -28,6 +28,8 @@ import { Icon } from 'rmwc/Icon'; import users from '../../users'; import { setUser, userFromMatch } from '../../lib/url'; +import './Menu.scss'; + class Menu extends React.Component { static propTypes = { // redux @@ -59,19 +61,21 @@ class Menu extends React.Component { render() { return ( - <SimpleMenu - handle={<Button><ButtonIcon use="more_vert" /></Button>} - onSelected={(event) => { - // Send the `data-type` of the selected <MenuItem /> - this.onItemSelected(event.detail.item.dataset.type); - }} - > - <MenuItem data-type="add_label"><Icon use="bookmark_border" />Voeg label toe</MenuItem> - <MenuItem data-type="make_favorite"><Icon use="star_border" />Maak favoriet</MenuItem> - <div className="mdc-list-divider" role="separator" /> - <MenuItem data-type="room_finder"><Icon use="location_searching" />Lokaal zoeken</MenuItem> - <MenuItem data-type="use_legacy_schedule"><Icon use="launch" />Oud rooster gebruiken</MenuItem> - </SimpleMenu> + <div className="Menu"> + <SimpleMenu + handle={<Button><ButtonIcon use="more_vert" /></Button>} + onSelected={(event) => { + // Send the `data-type` of the selected <MenuItem /> + this.onItemSelected(event.detail.item.dataset.type); + }} + > + <MenuItem data-type="add_label"><Icon use="bookmark_border" />Voeg label toe</MenuItem> + <MenuItem data-type="make_favorite"><Icon use="star_border" />Maak favoriet</MenuItem> + <div className="mdc-list-divider" role="separator" /> + <MenuItem data-type="room_finder"><Icon use="location_searching" />Lokaal zoeken</MenuItem> + <MenuItem data-type="use_legacy_schedule"><Icon use="launch" />Oud rooster gebruiken</MenuItem> + </SimpleMenu> + </div> ); } } diff --git a/src/client/react/components/container/Menu.scss b/src/client/react/components/container/Menu.scss new file mode 100644 index 0000000..3d84507 --- /dev/null +++ b/src/client/react/components/container/Menu.scss @@ -0,0 +1,25 @@ +.Menu { + .mdc-menu-anchor { + height: 100%; + } + + .mdc-button { + height: 100%; + min-width: unset; + color: black; + + &::before, &::after { + background-color: black; + } + + i { + font-size: 24px; + } + } + + .mdc-list-item { + i { + padding-right: 8px; + } + } +} diff --git a/src/client/react/components/container/Results.js b/src/client/react/components/container/Results.js index 4fc9987..82e37cb 100644 --- a/src/client/react/components/container/Results.js +++ b/src/client/react/components/container/Results.js @@ -28,6 +28,8 @@ import users from '../../users'; import { setUser, userFromMatch } from '../../lib/url'; import Result from '../presentational/Result'; +import './Results.scss'; + class Results extends React.Component { static propTypes = { results: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -56,8 +58,8 @@ class Results extends React.Component { return ( <div - className={classnames('search__results', { - 'search__results--has-results': !isExactMatch && this.props.results.length > 0, + className={classnames('Results', { + hasResults: !isExactMatch && this.props.results.length > 0, })} style={{ minHeight: isExactMatch ? 0 : this.props.results.length * 54, diff --git a/src/client/react/components/container/Results.scss b/src/client/react/components/container/Results.scss new file mode 100644 index 0000000..60379cf --- /dev/null +++ b/src/client/react/components/container/Results.scss @@ -0,0 +1,5 @@ +.Results { + &.hasResults { + border-top: 1px #BDBDBD solid; + } +} diff --git a/src/client/react/components/container/RoomFinder.js b/src/client/react/components/container/RoomFinder.js index 97ed175..b729ee3 100644 --- a/src/client/react/components/container/RoomFinder.js +++ b/src/client/react/components/container/RoomFinder.js @@ -26,6 +26,8 @@ import { Button, ButtonIcon } from 'rmwc/Button'; import users from '../../users'; import { setUser, userFromMatch } from '../../lib/url'; +import './RoomFinder.scss'; + class RoomFinder extends React.Component { static propTypes = { // redux @@ -38,12 +40,6 @@ class RoomFinder extends React.Component { history: PropTypes.object.isRequired, } - constructor(props) { - super(props); - - this.changeRoom = this.changeRoom.bind(this); - } - componentWillMount() { const user = userFromMatch(this.props.match); if (this.props.isVisible && users.byId[user].type !== 'r') { @@ -81,12 +77,12 @@ class RoomFinder extends React.Component { } return ( - <div className="room-finder"> + <div className="RoomFinder"> <Button onClick={() => this.changeRoom(-1)}>Vorige</Button> <Button onClick={() => this.changeRoom(+1)}>Volgende</Button> <div className="grow" /> <Button - className="close-button" + className="closeButton" onClick={() => this.props.dispatch({ type: 'ROOM_FINDER/HIDE' })} > <ButtonIcon use="close" /> diff --git a/src/client/react/components/container/RoomFinder.scss b/src/client/react/components/container/RoomFinder.scss new file mode 100644 index 0000000..d4b16d9 --- /dev/null +++ b/src/client/react/components/container/RoomFinder.scss @@ -0,0 +1,23 @@ +.RoomFinder { + display: flex; + margin: 8px; + padding: 8px; + border-radius: 2px; + background-color: #D32F2F; + + .mdc-button { + color: white; + + &::before, &::after { + background-color: white; + } + } + + .closeButton { + min-width: 48px; + + .mdc-button__icon { + margin-right: 0; + } + } +} diff --git a/src/client/react/components/container/Search.js b/src/client/react/components/container/Search.js index c968957..a124b21 100644 --- a/src/client/react/components/container/Search.js +++ b/src/client/react/components/container/Search.js @@ -33,6 +33,8 @@ import Menu from './Menu'; import Results from './Results'; import IconFromUserType from '../presentational/IconFromUserType'; +import './Search.scss'; + class Search extends React.Component { static propTypes = { results: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -143,17 +145,17 @@ class Search extends React.Component { searchText === users.byId[urlUser].value; return ( - <div className="search"> - <div className={classnames('search-overflow', { 'search--has-focus': hasFocus })}> - <div className="search__input-wrapper"> - <div className="search__icon-wrapper"> + <div className="Search"> + <div className={classnames('overflow', { hasFocus })}> + <div className="inputWrapper"> + <div className="iconWrapper"> <IconFromUserType userType={isExactMatch ? users.byId[urlUser].type : null} defaultIcon={<SearchIcon />} /> </div> <input - id="search__input" + id="searchInput" onChange={event => dispatch({ type: 'SEARCH/INPUT_CHANGE', searchText: event.target.value })} onKeyDown={this.onKeyDown} value={searchText} diff --git a/src/client/react/components/container/Search.scss b/src/client/react/components/container/Search.scss new file mode 100644 index 0000000..ef629c2 --- /dev/null +++ b/src/client/react/components/container/Search.scss @@ -0,0 +1,42 @@ +.Search { + height: 54px; + position: relative; + + .overflow { + border-radius: 2px; + background-color: white; + position: absolute; + width: 100%; + box-shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); + + &.hasFocus { + box-shadow: 0 3px 8px 0 rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.08); + } + + .inputWrapper { + display: flex; + height: 54px; + + .iconWrapper { + height: 54px; + padding: 15px; + + svg { + height: 24px; + width: 24px; + } + } + + input { + border: 0; + background-color: transparent; + flex-grow: 1; + height: inherit; + padding: 16px; + padding-left: 0px; + font-size: 16px; + outline: none; + } + } + } +} diff --git a/src/client/react/components/container/WeekSelector.js b/src/client/react/components/container/WeekSelector.js index 9a88af6..a8f5936 100644 --- a/src/client/react/components/container/WeekSelector.js +++ b/src/client/react/components/container/WeekSelector.js @@ -29,6 +29,8 @@ import ArrowForwardIcon from 'react-icons/lib/md/arrow-forward'; import purifyWeek from '../../lib/purifyWeek'; import { setWeek, weekFromLocation } from '../../lib/url'; +import './WeekSelector.scss'; + class WeekSelector extends React.Component { static propTypes = { // react-router @@ -67,7 +69,7 @@ class WeekSelector extends React.Component { render() { return ( - <div className="week-selector"> + <div className="WeekSelector"> <button onClick={() => this.updateWeek(-1)}><ArrowBackIcon /></button> <div className="text">{this.getWeekText()}</div> <button onClick={() => this.updateWeek(+1)}><ArrowForwardIcon /></button> diff --git a/src/client/react/components/container/WeekSelector.scss b/src/client/react/components/container/WeekSelector.scss new file mode 100644 index 0000000..dd71fea --- /dev/null +++ b/src/client/react/components/container/WeekSelector.scss @@ -0,0 +1,40 @@ +.WeekSelector { + display: flex; + padding: 8px; + padding-bottom: 0; + background-color: #F44336; + color: white; + align-items: center; + + .text { + flex-grow: 1; + text-align: center; + } + + button { + background-color: initial; + border: initial; + color: inherit; + padding: 8px; + border-radius: 4px; + + svg { + font-size: 2em; + } + + &:focus { + background-color: #D32F2F; + outline: none; + } + + &:active { + background-color: #B81111; + outline: none; + } + + &::-moz-focus-inner { + /* Remove the dotted line outline from Firefox */ + border: 0; + } + } +} diff --git a/src/client/react/components/page/Index.js b/src/client/react/components/page/Index.js index ac6c4f6..b2ebbcb 100644 --- a/src/client/react/components/page/Index.js +++ b/src/client/react/components/page/Index.js @@ -22,10 +22,12 @@ import React from 'react'; import Search from '../container/Search'; import HelpBox from '../container/HelpBox'; +import './Index.scss'; + class IndexPage extends React.Component { render() { return ( - <div className="page-index"> + <div className="IndexPage"> <div className="container"> <img src="/icons/mml-logo.png" alt="Metis" /> <Search /> diff --git a/src/client/react/components/page/Index.scss b/src/client/react/components/page/Index.scss new file mode 100644 index 0000000..f47b36d --- /dev/null +++ b/src/client/react/components/page/Index.scss @@ -0,0 +1,33 @@ +.IndexPage { + background-color: #ececec; + padding-top: calc(50vh - 310px); + height: 100vh; + + .container { + max-width: 600px; + margin: 0 auto; + padding: 8px; + + img { + display: block; + margin: 64px auto; + } + + .Search { + z-index: 1; // Position search above help-box + } + } +} + + +@media (max-height: 510px) { + .IndexPage { + padding-top: 0px; + + .container { + img { + display: none; + } + } + } +} diff --git a/src/client/react/components/page/User.js b/src/client/react/components/page/User.js index 00fde7e..6f9373f 100644 --- a/src/client/react/components/page/User.js +++ b/src/client/react/components/page/User.js @@ -28,6 +28,8 @@ import { userFromMatch } from '../../lib/url'; import WeekSelector from '../container/WeekSelector'; import RoomFinder from '../container/RoomFinder'; +import './User.scss'; + class UserPage extends React.Component { static propTypes = { // react-router @@ -43,15 +45,15 @@ class UserPage extends React.Component { } return ( - <div className="page-user"> - <div className="search-wrapper"> - <div className="search-container"> + <div className="UserPage"> + <div className="searchWrapper"> + <div className="searchContainer"> <Search /> </div> </div> <Elevation z={2}> - <div className="menu"> - <div className="menu-container"> + <div className="headerWrapper"> + <div className="header"> <RoomFinder /> <WeekSelector /> </div> diff --git a/src/client/react/components/page/User.scss b/src/client/react/components/page/User.scss new file mode 100644 index 0000000..f56a593 --- /dev/null +++ b/src/client/react/components/page/User.scss @@ -0,0 +1,24 @@ +.UserPage { + .searchWrapper { + position: fixed; + z-index: 1; // Position the search bar abore the schedule + width: 100%; + + .searchContainer { + max-width: 600px; + margin: 0 auto; + padding: 8px; + } + } + + .headerWrapper { + background-color: #F44336; + padding-top: 54px; + + .header { + max-width: 600px; + margin: 0 auto; + padding: 8px; + } + } +} diff --git a/src/client/react/components/presentational/Loading.js b/src/client/react/components/presentational/Loading.js index e33720b..ce8f49a 100644 --- a/src/client/react/components/presentational/Loading.js +++ b/src/client/react/components/presentational/Loading.js @@ -24,7 +24,7 @@ import { LinearProgress } from 'rmwc/LinearProgress'; class Loading extends React.Component { render() { return ( - <div className="loading"> + <div className="Loading"> <LinearProgress determinate={false} /> </div> ); diff --git a/src/client/react/components/presentational/Loading.scss b/src/client/react/components/presentational/Loading.scss new file mode 100644 index 0000000..5748899 --- /dev/null +++ b/src/client/react/components/presentational/Loading.scss @@ -0,0 +1,5 @@ +.Loading { + .mdc-linear-progress .mdc-linear-progress__bar-inner { + background-color: #9C27B0; + } +} diff --git a/src/client/react/components/presentational/Result.js b/src/client/react/components/presentational/Result.js index 18e4eb9..900d3ac 100644 --- a/src/client/react/components/presentational/Result.js +++ b/src/client/react/components/presentational/Result.js @@ -25,6 +25,8 @@ import users from '../../users'; import IconFromUserType from './IconFromUserType'; +import './Result.scss'; + class Result extends React.Component { static propTypes = { userId: PropTypes.string.isRequired, @@ -34,20 +36,21 @@ class Result extends React.Component { render() { return ( - // eslint-disable-next-line + /* eslint-disable jsx-a11y/click-events-have-key-events */ + /* eslint-disable jsx-a11y/no-static-element-interactions */ <div - className={classnames('search__result', { - 'search__result--selected': this.props.isSelected, + className={classnames('Result', { + isSelected: this.props.isSelected, })} onClick={this.props.onClick} > - <div className="search__icon-wrapper"> + <div className="iconWrapper"> <IconFromUserType userType={users.byId[this.props.userId].type} /> </div> - <div className="search__result__text"> + <div className="text"> {users.byId[this.props.userId].value} {users.byId[this.props.userId].alt && - <span className="search__result__text__alt"> + <span className="alt"> {` ${users.byId[this.props.userId].alt}`} </span> } diff --git a/src/client/react/components/presentational/Result.scss b/src/client/react/components/presentational/Result.scss new file mode 100644 index 0000000..1ca0dd8 --- /dev/null +++ b/src/client/react/components/presentational/Result.scss @@ -0,0 +1,30 @@ +.Result { + display: flex; + cursor: pointer; + + &:hover, &.isSelected { + background-color: lightgray; + } + + .iconWrapper { + height: 54px; + padding: 15px; + + svg { + height: 24px; + width: 24px; + } + } + + .text { + padding: 15px; + padding-left: 0px; + font-size: 16px; + transform: translate(0, 3px); + + .alt { + font-style: italic; + color: gray; + } + } +} diff --git a/src/client/react/index.js b/src/client/react/index.js index 566c847..2a66f82 100644 --- a/src/client/react/index.js +++ b/src/client/react/index.js @@ -18,13 +18,14 @@ * */ +import 'babel-polyfill'; +import 'whatwg-fetch'; import React from 'react'; import ReactDOM from 'react-dom'; import moment from 'moment'; -import { createStore, applyMiddleware, compose } from 'redux'; +import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import logger from 'redux-logger'; import { BrowserRouter as Router, @@ -37,14 +38,20 @@ import reducer from './reducers'; import Index from './components/page/Index'; import User from './components/page/User'; +import './index.scss'; + +// Set the locale for moment.js to dutch. This ensures that the correct week +// number logic is used. moment.locale('nl'); -// eslint-disable-next-line no-underscore-dangle -const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; +/* eslint-disable no-underscore-dangle */ const store = createStore( reducer, - composeEnhancers(applyMiddleware(logger)), + // Redux devtools extension + // https://github.com/zalmoxisus/redux-devtools-extension + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(), ); +/* eslint-enable no-underscore-dangle */ ReactDOM.render( <Provider store={store}> @@ -56,9 +63,9 @@ ReactDOM.render( </Switch> </Router> </Provider>, - document.getElementById('root'), + document.querySelector('#root'), ); // We only want to focus the input on page load. NOT on a in-javascript // redirect. This is because that is when people usually want to start typing. -document.querySelector('.search input').focus(); +document.querySelector('#searchInput').focus(); diff --git a/src/client/react/index.scss b/src/client/react/index.scss new file mode 100644 index 0000000..4042d06 --- /dev/null +++ b/src/client/react/index.scss @@ -0,0 +1,12 @@ +* { + box-sizing: border-box; +} + +body { + font-family: 'Roboto'; + margin: 0; +} + +:global(.grow) { + flex-grow: 1; +} diff --git a/src/client/static/browserconfig.xml b/src/client/static/browserconfig.xml index b3930d0..6a1d00e 100644 --- a/src/client/static/browserconfig.xml +++ b/src/client/static/browserconfig.xml @@ -2,7 +2,7 @@ <browserconfig> <msapplication> <tile> - <square150x150logo src="/mstile-150x150.png"/> + <square150x150logo src="/icons/mstile-150x150.png"/> <TileColor>#da532c</TileColor> </tile> </msapplication> diff --git a/src/client/static/apple-touch-icon.png b/src/client/static/icons/apple-touch-icon.png Binary files differindex 5adfc69..5adfc69 100644 --- a/src/client/static/apple-touch-icon.png +++ b/src/client/static/icons/apple-touch-icon.png diff --git a/src/client/static/favicon-16x16.png b/src/client/static/icons/favicon-16x16.png Binary files differindex 1df47d3..1df47d3 100644 --- a/src/client/static/favicon-16x16.png +++ b/src/client/static/icons/favicon-16x16.png diff --git a/src/client/static/favicon-32x32.png b/src/client/static/icons/favicon-32x32.png Binary files differindex 36cd5da..36cd5da 100644 --- a/src/client/static/favicon-32x32.png +++ b/src/client/static/icons/favicon-32x32.png diff --git a/src/client/static/mstile-150x150.png b/src/client/static/icons/mstile-150x150.png Binary files differindex 5e381e6..5e381e6 100644 --- a/src/client/static/mstile-150x150.png +++ b/src/client/static/icons/mstile-150x150.png diff --git a/src/client/static/safari-pinned-tab.svg b/src/client/static/icons/safari-pinned-tab.svg index 97ce8bf..97ce8bf 100644 --- a/src/client/static/safari-pinned-tab.svg +++ b/src/client/static/icons/safari-pinned-tab.svg diff --git a/src/client/static/manifest.beta.webmanifest b/src/client/static/manifest.beta.webmanifest deleted file mode 100644 index a1fdd92..0000000 --- a/src/client/static/manifest.beta.webmanifest +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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.json index d33ee8e..d33ee8e 100644 --- a/src/client/static/manifest.webmanifest +++ b/src/client/static/manifest.json diff --git a/src/client/static/sw.js b/src/client/static/sw.js deleted file mode 100644 index bd43805..0000000 --- a/src/client/static/sw.js +++ /dev/null @@ -1,29 +0,0 @@ -/* 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 deleted file mode 100644 index d74a7aa..0000000 --- a/src/client/static/untisinfo.css +++ /dev/null @@ -1,11 +0,0 @@ -html, body { - overflow: auto; - width: 100vw; - height: 100vh; - margin: 0; - -webkit-overflow-scrolling: touch; -} - -center { - margin: 5px; -} diff --git a/src/client/views/index.handlebars b/src/client/views/index.handlebars index be6a075..5105439 100644 --- a/src/client/views/index.handlebars +++ b/src/client/views/index.handlebars @@ -24,7 +24,6 @@ <title>Metis Rooster</title> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css"> - <link rel="stylesheet" href="/bundle.css"> </head> <body> <div id="root"></div> diff --git a/src/client/views/partials/head.handlebars b/src/client/views/partials/head.handlebars index 9801a95..9cf4023 100644 --- a/src/client/views/partials/head.handlebars +++ b/src/client/views/partials/head.handlebars @@ -23,8 +23,8 @@ <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"> +<link rel="manifest" href="/manifest.json"> +<link rel="apple-touch-icon" sizes="120x120" href="/icons/apple-touch-icon.png"> +<link rel="icon" type="image/png" href="/icons/favicon-32x32.png" sizes="32x32"> +<link rel="icon" type="image/png" href="/icons/favicon-16x16.png" sizes="16x16"> +<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#f44336"> diff --git a/src/server/lib/schools/hetmml/parseSchedule.js b/src/server/lib/schools/hetmml/parseSchedule.js new file mode 100644 index 0000000..14e861e --- /dev/null +++ b/src/server/lib/schools/hetmml/parseSchedule.js @@ -0,0 +1,100 @@ +const { JSDOM } = require('jsdom'); + +function fixFirstLargeScheduleItem(trNodeList) { + return Array.from(trNodeList).some((trNode, timeOfDay) => { + const tdNodeList = trNode.children; + + return Array.from(tdNodeList).some((tdNode, dayOfWeek) => { + const height = tdNode.rowSpan / 2; + if (height === 1) { + return false; + } + + tdNode.rowSpan = 2; // eslint-disable-line no-param-reassign + + for (let i = 1; i < height; i += 1) { + // Are we at the end of the table? + if (dayOfWeek === 4) { + // If so, we cannot use insertBefore, because the is no node to insert + // it before. Use appendChild instead. + trNodeList[timeOfDay + i].appendChild(tdNode.cloneNode(true)); + } else { + trNodeList[timeOfDay + i] + .insertBefore( + tdNode.cloneNode(true), + trNodeList[timeOfDay + i].children[dayOfWeek], + ); + } + } + + return true; + }); + }); +} + +function parseSchedule(axiosResponse) { + const dom = new JSDOM(axiosResponse.data); + const { window } = dom; + const { document } = window; + + const tableNode = document.querySelector('center > table'); + const tbodyNode = tableNode.querySelector('tbody'); + const trNodeList = tbodyNode.children; + + Array.from(trNodeList).forEach((trNode, timeOfDay) => { + const tdNodeList = trNode.children; + + if (timeOfDay === 0 || trNode.children.length === 0) { + tbodyNode.removeChild(trNode); + return; + } + + Array.from(tdNodeList).forEach((tdNode, dayOfWeek) => { + if (dayOfWeek === 0) { + trNode.removeChild(tdNode); + } + }); + }); + + let shouldContinue = true; + while (shouldContinue) { + shouldContinue = fixFirstLargeScheduleItem(trNodeList); + } + + const scheduleItems = []; + + Array.from(trNodeList).forEach((trNode, timeOfDay) => { + const tdNodeList = trNode.children; + Array.from(tdNodeList).forEach((tdNode, dayOfWeek) => { + if (tdNode.textContent.trim() === '') { + return; + } + + const childTableNode = tdNode.querySelector('table'); + const childTrNodeList = childTableNode.querySelectorAll('tr'); + + Array.from(childTrNodeList).forEach((childTrNode) => { + const subject = childTrNode.children[0].textContent.trim(); + const attendees = childTrNode.children[1] + ? childTrNode.children[1].textContent.trim() + : undefined; + const location = childTrNode.children[2] + ? childTrNode.children[2].textContent.trim() + : undefined; + + scheduleItems.push({ + startTime: timeOfDay, + endTime: timeOfDay + 1, + dayOfWeek, + subject, + attendees, + location, + }); + }); + }); + }); + + return scheduleItems; +} + +module.exports = parseSchedule; diff --git a/src/server/routes/getSchedule.js b/src/server/routes/getSchedule.js index 04df28a..7a84dd2 100644 --- a/src/server/routes/getSchedule.js +++ b/src/server/routes/getSchedule.js @@ -25,11 +25,14 @@ const router = express.Router(); const getScheduleData = require('../lib/schools/hetmml/getScheduleData'); const getURLOfUser = require('../lib/schools/hetmml/getURLOfUser'); const axios = require('../lib/schools/hetmml/axios'); +const parseSchedule = require('../lib/schools/hetmml/parseSchedule'); // 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) { +function currentWeekNumber() { + const target = new Date(); + const dayNr = (target.getDay() + 6) % 7; // eslint-disable-next-line target.setDate(target.getDate() - dayNr + 3); @@ -43,39 +46,58 @@ function getWeekNumber(target) { return 1 + Math.ceil((firstThursday - target) / 604800000); } +async function getSchedule(userType, userValue, week, scheduleType = 'dag') { + const { users } = await getScheduleData(); + const user = + users.filter(user_ => user_.type === userType && user_.value === userValue)[0]; + + if (!user) { + throw new Error(`${userType}/${userValue} is not in the user index.`); + } + + if (!week) { + week = currentWeekNumber(); // eslint-disable-line no-param-reassign + } + + const { index } = user; + const url = getURLOfUser(scheduleType, userType, index, week); + + return axios.get(url); +} + +router.get('/:type/:value.json', (req, res, next) => { + const { type, value } = req.params; + const { week, type: scheduleType } = req.query; + + getSchedule(type, value, week, scheduleType) + .then((response) => { + const schedule = parseSchedule(response); + res.json(schedule); + }) + .catch((err) => { + if (err.response) { + // eslint-disable-next-line no-param-reassign + err.status = err.response.status; + } + next(err); + }); +}); + router.get('/:type/:value', (req, res, next) => { - getScheduleData().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 scheduleType = req.query.type || 'dag'; - - const url = getURLOfUser(scheduleType, type, index, week); - - axios.get(url) - .then((response) => { - res.status(response.status).end(response.data); - }) - .catch((err) => { - if (err.response) { - // eslint-disable-next-line no-param-reassign - err.status = err.response.status; - } - next(err); - }); - }); + const { type, value } = req.params; + const { week, type: scheduleType } = req.query; + + getSchedule(type, value, week, scheduleType) + .then((response) => { + res.status(response.status).end(response.data); + }) + .catch((err) => { + if (err.response) { + // eslint-disable-next-line no-param-reassign + err.status = err.response.status; + } + next(err); + }); }); module.exports = router; |