├── .babelrc ├── .deploy_key.enc ├── .deploy_key.pub ├── .editorconfig ├── .env.sample ├── .eslintrc.yaml ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── browserslists ├── doc ├── README.md ├── api.md ├── img │ └── kos-attendance-list.png ├── porovnani.rst └── pozadavky.md ├── ictweb.yml ├── package.json ├── script ├── build-static ├── pre-deploy └── travis-deploy ├── src ├── Root.jsx ├── actions │ ├── clientActions.js │ ├── dataActions.js │ ├── filterActions.js │ ├── linkActions.js │ ├── searchActions.js │ ├── semesterActions.js │ ├── settingsActions.js │ ├── uiActions.js │ └── userActions.js ├── app.js ├── callbacks │ ├── faux.js │ ├── index.js │ └── sirius.js ├── client.js ├── components │ ├── Controls.jsx │ ├── Day.jsx │ ├── ErrorMessage.jsx │ ├── Event.jsx │ ├── EventDetail.jsx │ ├── Footer.jsx │ ├── FunctionFilter.jsx │ ├── FunctionSettings.jsx │ ├── FunctionsBar.jsx │ ├── FunctionsSidebar.jsx │ ├── Grid.jsx │ ├── Header.jsx │ ├── HourLabel.jsx │ ├── LogoutButton.jsx │ ├── NowIndicator.jsx │ ├── PeriodicUpdate.jsx │ ├── PositionedExpander.jsx │ ├── Search.jsx │ ├── SemesterWeek.jsx │ ├── SidebarIcal.jsx │ ├── Spinner.jsx │ ├── Timetable.jsx │ ├── Toggleable.jsx │ ├── UserMenu.jsx │ ├── WeekNav.jsx │ └── WeekSwitcher.jsx ├── config.js ├── constants │ ├── actionTypes.js │ ├── api.js │ ├── events.js │ ├── index.js │ ├── propTypes.js │ └── screenSizes.js ├── containers │ └── FittableContainer.jsx ├── dataManipulation.js ├── date.js ├── error-unauthorized.html ├── error.html ├── globalApi.js ├── images │ ├── landing-page-devices.png │ └── logo.png ├── index.html ├── landing.html ├── locale.js ├── locales │ ├── cs.json │ └── en.json ├── reducers │ ├── clientReducer.js │ ├── dataReducer.js │ ├── index.js │ ├── searchReducer.js │ ├── semesterReducer.js │ ├── settingsReducer.js │ ├── uiReducer.js │ └── userReducer.js ├── routes.js ├── screen.js ├── selectors │ └── routerSelector.js ├── semester.js ├── semesterWeeks.js ├── signpost.html ├── store │ ├── index.js │ └── persistence.js ├── stylesheets │ ├── _base.scss │ ├── _components.scss │ ├── _mixins.scss │ ├── _reset.scss │ ├── _settings.scss │ ├── components │ │ ├── _LogoutButton.scss │ │ ├── _SemesterWeek.scss │ │ ├── _SidebarIcal.scss │ │ ├── _error.scss │ │ ├── _flags-control.scss │ │ ├── _footer.scss │ │ ├── _function-filter.scss │ │ ├── _function-settings.scss │ │ ├── _functions-bar-responsive.scss │ │ ├── _functions-bar.scss │ │ ├── _functions-sidebar-responsive.scss │ │ ├── _functions-sidebar.scss │ │ ├── _grid.scss │ │ ├── _header-responsive.scss │ │ ├── _header.scss │ │ ├── _landing-page.scss │ │ ├── _search.scss │ │ ├── _signpost-page.scss │ │ ├── _spinner.scss │ │ ├── _table-animations.scss │ │ ├── _table-appearances.scss │ │ ├── _table-events-colors.scss │ │ ├── _table-events-detail.scss │ │ ├── _table-events.scss │ │ ├── _table-horizontal.scss │ │ ├── _table-responsive.scss │ │ ├── _table.scss │ │ ├── _ukraine-ribbon.scss │ │ ├── _week-nav-responsive.scss │ │ ├── _week-nav.scss │ │ └── _week-switcher.scss │ └── fittable.scss ├── time.js ├── timetable.js ├── utils.js └── utils │ ├── safeExpandingDirection.js │ └── suitClassName.js ├── test ├── .eslintrc.yaml ├── actions │ ├── clientActions.test.js │ ├── dataActions.test.js │ ├── filterActions.test.js │ ├── linkActions.test.js │ ├── searchActions.test.js │ ├── semesterActions.test.js │ ├── settingsActions.test.js │ ├── uiActions.test.js │ └── userActions.test.js ├── client.test.js ├── dataManipulation.test.js ├── date.test.js ├── helpers │ └── routerState.js ├── reducers │ ├── clientReducer.test.js │ ├── dataReducer.test.js │ ├── index.test.js │ ├── searchReducer.test.js │ ├── semesterReducer.test.js │ ├── uiReducer.test.js │ └── userReducer.test.js ├── screen.test.js ├── selectors │ └── routerSelector.test.js ├── semester.test.js ├── semesterWeeks.test.js ├── time.test.js ├── timetable.test.js ├── utils.test.js └── utils │ ├── safeExpandingDirection.test.js │ └── suitClassName.test.js ├── webpack.config.js ├── webpack.production.config.js ├── webpack └── config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "optional": ["runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.deploy_key.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvut/fittable/209389f08b01749cb81bf0d3215b7480354b9328/.deploy_key.enc -------------------------------------------------------------------------------- /.deploy_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDpz+wSLdX20arMJtKsgUG45PxKdoLojPYNz89i+Ts0XORHDuUjUSMdUCNtleUyoMpnVuoImUIv6njvkgYtbiuaSC8EnefIuEfbsysg4znJ6rT3/ZzsXT4yuEEuBhA4tc4nC6R/r/hkxjx2V+E3VZ06q36Sbx+JZxrxEYx36SmHuRk61cJOkyR0hNk3o2psiucTflSFGWG1cYlavxOdRYrpoJ7b6Db9H2I8pEG8dvtL2+klHiiScP9vHOQWCZkCJGwLt0VuP06ohzICbBFqbAqC2p+YBczZUUSxj9xgKKaniuCGUveV2vox9X29EzN2hYHb4a95XHYp2XIv2LgVZY4UcfDwFX1pudtfRdlL0zo8hSZZaeLfalEGCPQtDaSBqX/newj2k09DeETUHVa2Vy2/p4NBNBIASbIESWGYMy/5DTNRJI2AHIBd7MmEJJZtp6YvMTT34i8rwa7OMcl40iJgQCQGX/X5QqTFJmGaSiLEKhPaRZmF4GuI0mJvmNhN0hU= fittable@travis 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most editorconfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [/script/*] 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | OAUTH_USERNAME=MISSING 2 | OAUTH_ACCESS_TOKEN= 3 | FITTABLE_SOURCE=faux 4 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | extends: [standard, standard-react] 4 | env: 5 | browser: true 6 | node: true 7 | es6: true 8 | ecmaFeatures: 9 | modules: true 10 | rules: 11 | camelcase: [2] 12 | no-var: [1] 13 | prefer-const: [1] 14 | comma-dangle: [2, always-multiline] 15 | jsx-quotes: [2, prefer-double] 16 | curly: [2, all] 17 | max-len: [2, 120, 2] 18 | no-extra-bind: [2] 19 | max-len: [2, 100, 2, {ignoreComments: true, ignoreUrls: true}] 20 | padded-blocks: [0] 21 | react/prop-types: [0] 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Environment configuration ### 2 | .env 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Dependency directory 15 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 16 | node_modules/* 17 | 18 | ### Sass ### 19 | .sass-cache 20 | *.css.map 21 | 22 | ### JetBrains ### 23 | *.iml 24 | .idea/ 25 | 26 | ### Archives ### 27 | *.gz 28 | *.zip 29 | 30 | ### Windows ### 31 | # Windows image file caches 32 | Thumbs.db 33 | ehthumbs.db 34 | 35 | # Folder config file 36 | Desktop.ini 37 | 38 | # Recycle Bin used on file shares 39 | $RECYCLE.BIN/ 40 | 41 | ### OSX ### 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Thumbnails 47 | ._* 48 | 49 | ### Linux ### 50 | *~ 51 | 52 | # KDE directory preferences 53 | .directory 54 | 55 | ### Project-specific ### 56 | 57 | # Continuous deployment 58 | .deploy_key 59 | 60 | # Compiled files 61 | dist/ 62 | .tmp 63 | 64 | # Temporary stuff 65 | tmp/ 66 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: gitlab.fit.cvut.cz:5000/ict/alpine-docker-images/ci:3.12 2 | 3 | stages: 4 | - build 5 | - test 6 | - deploy 7 | 8 | cache: 9 | key: "$CI_COMMIT_REF_SLUG" # per-branch cache 10 | paths: 11 | - .cache/ 12 | 13 | before_script: 14 | - apk add build-base nodejs npm python2 yarn 15 | - export PKG_CACHE_PATH=".cache/pkg-cache" 16 | # GitLab CI does not cache anything outside the project directory. 17 | - yarn config set cache-folder $(pwd)/.cache/yarn 18 | - yarn install 19 | 20 | build: 21 | stage: build 22 | script: 23 | - yarn run build 24 | artifacts: 25 | paths: 26 | - dist/* 27 | expire_in: 1 month 28 | 29 | test: 30 | stage: test 31 | script: 32 | - yarn run lint 33 | - yarn run test:spec 34 | 35 | fit-pages: 36 | stage: deploy 37 | only: 38 | - master 39 | dependencies: 40 | - build 41 | before_script: [] 42 | script: 43 | - ./script/pre-deploy 44 | environment: 45 | name: production 46 | url: https://timetable.fit.cvut.cz 47 | artifacts: 48 | paths: 49 | - public 50 | expire_in: 1 hour 51 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | dev/ 2 | dist/fittable.js 3 | dist/fittable.min.js 4 | dist/fittable.min.css 5 | dist/*.map 6 | script/ 7 | src/ 8 | .tmp/ 9 | .sass-cache/ 10 | browserslists 11 | docs/ 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @cvut:registry=https://repository.fit.cvut.cz/npm 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | sudo: false 5 | dist: trusty 6 | addons: 7 | apt: 8 | sources: 9 | - sourceline: deb https://dl.yarnpkg.com/debian/ stable main 10 | key_url: https://dl.yarnpkg.com/debian/pubkey.gpg 11 | packages: 12 | - yarn 13 | install: 14 | - yarn 15 | before_script: 16 | - yarn run build 17 | script: 18 | - yarn run lint 19 | - yarn run test:spec 20 | notifications: 21 | email: false 22 | slack: 23 | secure: hgkORyT7J3El7oJWlyXcMj8/nB3Wamfq6VFUaGMW2UMuTcgar5DXKB/Awnz/Ish2klgYx4icxKUBquLoEFc5hF5dWh7HxRpXAQWztoLs0RLwGIE12h1su5bt1h5UN1pIqGey+g8B0DddooCz7o8M1vwTATWq5mK1aCrjV3evQ0M= 24 | deploy: 25 | skip_cleanup: true 26 | provider: script 27 | script: script/travis-deploy 28 | on: 29 | branch: master 30 | env: 31 | global: 32 | - secure: UgTXgzJc9W9Ya3xH6N1jmRD7ojUOnWze5sNrr/7elAfC6asmERQJj9vK3A7Oby0DQekEB1WTH4iBA1uh8/cO/6G5GZqfoDq21acJVW5OtouPWwZSNc7/YUvMHiin7WlhBh9iGZBaZ0fzkaqrOv5daxhVApPwV3uFwECPey4HytA= 33 | - secure: SxxazkZH7nRNwUgu+JtQCu41MJkwZYtIz8bgwMRceUudNS/u44qXtBuz64rExoV8NHska0ISls6JlB9ReqiwlGi5nbbW37OkSZ+CcbVAsPQ9+KKCngmgJlVXKkdPZ9tD1av2M1w681tNnRGAiMJTwSyjPm8C7VkPdcL8hLRgBOU= 34 | - secure: iIC2f1+dO9EbXYAnwy9ffNRe0yfpJVHKGLTt7Dd1oxjm2BBPWdwLSrt3wpcJ/nqKVDbteC1Lfh7uiJnhcLOx56K4c5dpkJUSQGOx+kdlyHqFe7LAH6w9++IiOGvT8Ogqb+nA5j20HOkxoVHqgB+iiHNqNvpFxb/LoAEy1NWd3eA= 35 | cache: 36 | yarn: true 37 | directories: 38 | - node_modules 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2015-2016 Czech Technical University in Prague. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against this version of Node.js 2 | environment: 3 | nodejs_version: "4" 4 | 5 | version: "{build}-{branch}" 6 | 7 | shallow_clone: true 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | # Get the latest stable version of Node.js or io.js 12 | - ps: Install-Product node $env:nodejs_version x64 13 | # install modules 14 | - npm install -g npm@3 15 | - set PATH=%APPDATA%\npm;%PATH% 16 | - npm install 17 | 18 | # Post-install test scripts. 19 | test_script: 20 | # Output useful info for debugging. 21 | - node --version 22 | - npm --version 23 | # run tests 24 | - npm test 25 | 26 | cache: 27 | - node_modules -> package.json 28 | - "%APPDATA%\\npm-cache -> package.json" 29 | 30 | # Don't actually build. 31 | build: off 32 | deploy: off 33 | -------------------------------------------------------------------------------- /browserslists: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | ie >= 9 3 | Android 3 4 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | Fittable slouží k přehlednému zobrazení výuky jako kalendáře. Oproti stávající aplikaci Timetable, která zobrazuje výuku jako 14denní statický rozvrh hodin, Fittable zohledňuje: 2 | 3 | * Změny ve výuce (např. kvůli státnímu svátku), 4 | * posuny hodin (např. v místnostech které se řídí časovým rozvrhem FA), 5 | * jednorázové události a zkoušky. 6 | 7 | Fittable je _client-side_ webová aplikace psaná v JavaScriptu. Data jsou čerpaná z aplikace [Sirius](https://github.com/cvut/sirius). 8 | -------------------------------------------------------------------------------- /doc/api.md: -------------------------------------------------------------------------------- 1 | # Client-side API 2 | 3 | Fittable exposes some functionality for integration with native applications or client-side extensions. All methods are available in `window.fittable`. 4 | 5 | ## changeDate 6 | 7 | ```js 8 | changeDate(date: Date | string): void 9 | ``` 10 | 11 | Changes displayed date to a new date. A date can be a Date object or date string in ISO format, e.g. `2017-03-30` 12 | 13 | 14 | ### Example 15 | 16 | ```js 17 | // Set date to January 30th, 2017 18 | window.fittable.changeDate('2017-01-30') 19 | 20 | // Set date to today's date 21 | window.fittable.changeDate(new Date()) 22 | ``` 23 | 24 | ## getCurrentDate 25 | 26 | ```js 27 | getCurrentDate(): Date 28 | ``` 29 | 30 | Returns currently displayed date as Date object. 31 | 32 | ### Example 33 | 34 | ```js 35 | var d = window.fittable.getCurrentDate() 36 | console.log(d) 37 | // Prints: 38 | // Mon Mar 27 2017 02:00:00 GMT+0200 (CEST) 39 | ``` 40 | 41 | ## getCurrentDateStr 42 | 43 | ```js 44 | getCurrentDateStr(): string 45 | ``` 46 | 47 | Returns currently displayed date as Date object. 48 | 49 | ### Example 50 | 51 | ```js 52 | var s = window.fittable.getCurrentDateStr() 53 | console.log(s) 54 | // Prints: 55 | // 2017-03-27 56 | ``` 57 | -------------------------------------------------------------------------------- /doc/img/kos-attendance-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cvut/fittable/209389f08b01749cb81bf0d3215b7480354b9328/doc/img/kos-attendance-list.png -------------------------------------------------------------------------------- /doc/pozadavky.md: -------------------------------------------------------------------------------- 1 | # Požadavky 2 | 3 | Datum: Leden 2015
4 | Autor: Jakub Jirůtka 5 | 6 | ## Funkční požadavky 7 | 8 | 1. bude poskytovat náhled na události (tj. kalendáře) z pohledu: 9 | 1. osoby, 10 | 2. předmětu, 11 | 3. místnosti. 12 | 2. bude poskytovat různé módy zobrazení: 13 | 1. rozvrhový (horizontální): 14 | a. s přepínáním mezi zobrazení 5/7 dní (např. dálkoví studenti mívají výuku i o víkendu), 15 | b. s časovou osou podle harmonogramu dané fakulty (_časové_ rozsahy) sloužící jako _pomocné_ vodítko. 16 | 2. kalendářní: 17 | 1. týdenní (výchozí), 18 | 2. _měsíční,_ (**P2**) 19 | 3. denní. 20 | 3. bude zobrazovat paritu (sudý/lichý) aktuálně vybraného období. 21 | 4. bude zobrazovat označení semestru aktuálně vybraného období. 22 | 5. okénko události bude obsahovat: 23 | 1. kód předmětu, 24 | 2. místnost (KOSí kód), (**P1**) 25 | 3. čas začátku a _konce události_ (**P2**), 26 | 4. typ události (barevným rozlišením), 27 | 5. příznak, zda byla událost ovlivněna rozvrhovou výjimkou (tj. změna oproti pravidelnému rozvrhu). 28 | 6. po kliknutí na okénko události se zobrazí detail události obsahující: 29 | 1. celý název předmětu nebo události, 30 | 2. odkaz na kalendář předmětu (jen pro P/C/L/J/Z), 31 | 3. pořadové číslo výukové hodiny nebo zkoušky v semestru (jen pro P/C/L/Z), 32 | 4. číslo paralelky (jen pro P/C/L/J), 33 | 5. odkaz na kalendář místnosti, 34 | 6. vyučující/zkoušející/organizátoři události (jeden nebo více lidí): 35 | 1. celé jméno, 36 | 2. e-mail (je na Usermapu), 37 | 3. odkaz na kalendář, 38 | 4. odkaz na Usermap. 39 | 7. seznam studentů v paralelce (jen pro P/C/L): (**P3**) 40 | 1. celé jméno, 41 | 2. uživatelské jméno, 42 | 3. zobrazovat ve „fancy pop-up.“ 43 | 8. seznam výjimek, které událost ovlivnily. 44 | 7. na původním místě zrušené či přesunuté události bude zobrazovat zástupné okýnko: 45 | 1. v případě přesunuté události s odkazem na novou událost. (**P3**) 46 | 8. bude umožňovat navigaci v kalendáři po týdnech/měsících/semestrech. 47 | 9. bude poskytovat filtr zobrazených událostí: 48 | 1. podle typu události. 49 | 10. bude poskytovat jednotný vyhledávač kalendářů: 50 | 1. osoby (podle uživatelského i občanského jména), 51 | 2. předmětu (podle kódu a názvu), 52 | 3. místnosti (podle KOSího kódu). 53 | 11. kalendářní data bude získávat ze služby Sirius: 54 | 1. přes jeho RESTful API, ve formátu JSON, 55 | 2. _on-demand_. 56 | 12. bude poskytovat JS API pro: 57 | 1. přidání položek zobrazených v okénku události, 58 | 2. přidání položek zobrazených v detailu události (textových i akcí), 59 | 3. odchytávání akcí nad kalendářem [TODO]. 60 | 61 | ## Nefunkční požadavky 62 | 63 | 1. Uživatelské rozhraní: 64 | 1. bude lokalizované do češtiny a angličtiny, 65 | 2. bude responzivní (od desktopu po mobilní telefon s WVGA), 66 | 3. bude přizpůsobené i pro ovládání na dotykovém displeji, 67 | 4. vizuálně bude vycházet z grafiky vytvořené pro „nový web“ od Josefa Lobotky (fonty, barvy, příp. ikony). 68 | 2. Přizpůsobitelnost a konfigurovatelnost: 69 | 1. widget bude možné přizpůsobit pro různá použití: 70 | 1. na fakultním webu pro studenty a vyučující, 71 | 2. v rámci administračního rozhraní pro rozvrháře, 72 | 3. v módu omezené funkčnosti, např. na osobní stránce vyučujícího, studenta atp. 73 | 2. widget bude počítat s nasazením i na dalších fakultách. 74 | 3. Dokumentace: 75 | 1. bude obsahovat kompletní postup pro sestavení (build), 76 | 2. bude obsahovat popis konfigurace a JS API pro přizpůsobení widgetu, 77 | 3. veškerá dokumentace by měla být v angličtině. 78 | 4. Kvalita kódu: 79 | 1. kód bude srozumitelný, dobře strukturovaný a potenciálně nejasné části okomentované, 80 | 2. kód bude pokrytý jednotkovými a integračními testy, 81 | 3. kód i další textové soubory budou v UTF-8 a s unixovým způsobem ukončování řádek (řídící znak LF / 0x0A), 82 | 3. názvy všech identifikátorů použitých v kódu budou v angličtině. 83 | 5. Verzování: 84 | 1. projekt bude verzovaný systémem Git na GitHubu. 85 | 6. Technologie: 86 | 1. bude implementovaný v jazyce kompilovaném do JavaScriptu; preferovaná je syntaxe ECMAScript 6+ (s překladem pomocí https://github.com/babel/babel[Babel]). 87 | 7. Běhové prostředí: 88 | 1. bude fungovat _client-side_, ve webovém prohlížeči s podporou JavaScriptu: 89 | 1. Chrome/Chromium 35+, 90 | 2. Firefox 31+, 91 | 3. Internet Explorer 10+ (9 alespoň omezeně), 92 | 4. Safari 6+. 93 | 8. Licence: 94 | 1. bude využívat výhradně _open-source_ knihovny. 95 | 96 | ## Zkratky 97 | 98 | Typy událostí: 99 | 100 | * přednáška [P] 101 | * cvičení [C] 102 | * laboratoř [L] 103 | * jednorázová akce předmětu [J] 104 | * zkouška (i zápočtová) [Z] 105 | * omezení vyučujícího [V] 106 | * obecná událost [O] 107 | -------------------------------------------------------------------------------- /ictweb.yml: -------------------------------------------------------------------------------- 1 | name_cs: 'Fittable' 2 | description_cs: 'Aplikace pro zobrazení kalendáře výuky; technická a uživatelská dokumentace.' 3 | name_en: 'Fittable' 4 | description_en: 'Application for displaying study schedule; documentation for users and developers.' 5 | dir_index: 6 | - README.md 7 | doc_root: doc 8 | icon: icon.png 9 | keywords: 10 | - fittable 11 | - timetable 12 | - rozvrh 13 | - kalendář 14 | exclude: 15 | - img 16 | - include 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cvut/fittable", 3 | "version": "0.8.1", 4 | "description": "Flexible calendar widget for fittable written in JavaScript", 5 | "main": "dist/app.js", 6 | "author": "Marián Hlaváč", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/cvut/fittable" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/cvut/fittable/issues" 13 | }, 14 | "scripts": { 15 | "lint": "eslint src test --ext .js,.jsx", 16 | "clean": "rimraf dist/", 17 | "prebuild": "npm run clean", 18 | "build": "npm-run-all -p build:prod build:assets", 19 | "build:dev": "webpack -d", 20 | "build:prod": "webpack --optimize-minimize --config webpack.production.config.js --bail", 21 | "build:npm": "babel --out-dir dist src", 22 | "build:assets": "node script/build-static", 23 | "start": "npm run build:assets && webpack-dev-server --content-base dist/ --hot -d", 24 | "watch": "webpack -d --watch", 25 | "watch:npm": "babel -w --out-dir dist src", 26 | "test": "babel-tape-runner \"test/**/*.test.js\"", 27 | "test:watch": "chokidar --initial \"src/**/*.js\" \"src/**/*.js\" \"test/**/*.js\" -c \"npm test | tap-bail\"", 28 | "test:spec": "npm test | tap-spec" 29 | }, 30 | "dependencies": { 31 | "babel-runtime": "^5.8.25", 32 | "camelize": "^1.0.0", 33 | "counterpart": "^0.17.0", 34 | "file-loader": "^0.8.4", 35 | "font-awesome": "^4.4.0", 36 | "frozen-moment": "^0.4.0", 37 | "hammerjs": "^2.0.4", 38 | "history": "^1.13.1", 39 | "moment": "^2.15.1", 40 | "moment-timezone": "^0.5.6", 41 | "ramda": "^0.19.0", 42 | "raven-js": "^2.3.0", 43 | "react": "^0.14.0", 44 | "react-addons-css-transition-group": "^0.14.3", 45 | "react-cookie": "^0.3.4", 46 | "react-dom": "^0.14.0", 47 | "react-redux": "^4.0.0", 48 | "react-router": "^1.0.0-rc3", 49 | "redux": "^3.0.4", 50 | "redux-localstorage": "^1.0.0-rc4", 51 | "redux-localstorage-filter": "^0.1.1", 52 | "redux-router": "^1.0.0-beta3", 53 | "redux-thunk": "^1.0.0" 54 | }, 55 | "devDependencies": { 56 | "autoprefixer-loader": "^3.1.0", 57 | "babel": "^5.8.23", 58 | "babel-core": "^5.8.25", 59 | "babel-eslint": "^4.1.3", 60 | "babel-loader": "^5.3.2", 61 | "babel-tape-runner": "^1.2.0", 62 | "blue-tape": "^0.1.10", 63 | "chokidar-cli": "^1.1.0", 64 | "css-loader": "^0.21.0", 65 | "dotenv": "^1.2.0", 66 | "eslint": "^1.7.3", 67 | "eslint-config-standard": "^4.4.0", 68 | "eslint-config-standard-react": "^1.1.0", 69 | "eslint-plugin-react": "^3.6.3", 70 | "eslint-plugin-standard": "^1.3.1", 71 | "extract-text-webpack-plugin": "^0.8.2", 72 | "foundation-sites": "^5.5.3", 73 | "json-loader": "^0.5.3", 74 | "node-sass": "^4.14.1", 75 | "npm-run-all": "^1.2.12", 76 | "redux-logger": "^2.1.3", 77 | "rimraf": "^2.4.3", 78 | "sass-loader": "^3.0.0", 79 | "shelljs": "^0.5.3", 80 | "sinon": "^1.17.2", 81 | "style-loader": "^0.13.0", 82 | "tap-bail": "0.0.0", 83 | "tap-spec": "^4.1.0", 84 | "timekeeper": "0.0.5", 85 | "webpack": "^1.12.2", 86 | "webpack-dev-server": "^1.12.1" 87 | }, 88 | "license": "MIT" 89 | } 90 | -------------------------------------------------------------------------------- /script/build-static: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('shelljs/global') 3 | 4 | var baseDir = 'src' 5 | var targetDir = 'dist' 6 | 7 | var files = [ 8 | baseDir + '/index.html', 9 | baseDir + '/landing.html', 10 | baseDir + '/error-unauthorized.html', 11 | baseDir + '/signpost.html', 12 | baseDir + '/images', 13 | baseDir + '/locales', 14 | ] 15 | 16 | mkdir('-p', targetDir) 17 | cp('-Rf', files, targetDir) 18 | -------------------------------------------------------------------------------- /script/pre-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is temporary ugly workaround. 3 | set -eu 4 | 5 | cd "$(dirname "$0")/.." 6 | 7 | mkdir -p public 8 | cp -a dist public/new 9 | 10 | cd public 11 | mv new/signpost.html index.html 12 | 13 | md5js=$(md5sum new/fittable.js | cut -d' ' -f1) 14 | mv new/fittable.js new/fittable-$md5js.js 15 | 16 | md5css=$(md5sum new/fittable.css | cut -d' ' -f1) 17 | mv new/fittable.css new/fittable-$md5css.css 18 | 19 | sed -i "s|fittable.js|fittable-$md5js.js|" new/index.html 20 | sed -i "s|fittable.css|fittable-$md5css.css|" new/*.html 21 | sed -i "s|fittable.css|new/fittable-$md5css.css|" index.html 22 | sed -i 's|"_oauth/login"|"/_oauth/login?original_uri=/new/"|' new/landing.html 23 | sed -i 's|| \n |g' new/index.html 24 | -------------------------------------------------------------------------------- /script/travis-deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit if any statement returns a non-true return value. 4 | set -e 5 | 6 | if [[ "$TRAVIS_PULL_REQUEST" != 'false' ]]; then 7 | echo 'This is a pull request, skipping deploy.'; exit 0 8 | fi 9 | 10 | if [[ -z "$REMOTE" ]]; then 11 | echo '$REMOTE is not set, skipping deploy.'; exit 0 12 | fi 13 | 14 | if [[ "$TRAVIS_BRANCH" != 'master' ]]; then 15 | echo 'This is not the master branch, skipping deploy.'; exit 0 16 | fi 17 | 18 | if [[ "$TRAVIS_BUILD_NUMBER.1" != "$TRAVIS_JOB_NUMBER" ]]; then 19 | echo 'This is not a first job of the build, skipping deploy.'; exit 0 20 | fi 21 | 22 | openssl aes-256-cbc -K $encrypted_97a5f01c84d8_key -iv $encrypted_97a5f01c84d8_iv -in .deploy_key.enc -out .deploy_key -d 23 | chmod 600 .deploy_key 24 | 25 | echo "Deploying to the remote server." 26 | scp -Br -i .deploy_key -o StrictHostKeyChecking=no dist/* $REMOTE:/var/www/sirius/fittable-dev/ 27 | -------------------------------------------------------------------------------- /src/Root.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'react-redux' 3 | 4 | import createStore from './store' 5 | import FittableContainer from './containers/FittableContainer' 6 | import * as callbacks from './callbacks' 7 | import { browserLanguage } from './client' 8 | import { initialState as settingsInitialState } from './reducers/settingsReducer' 9 | 10 | const initialState = { 11 | settings: { 12 | ...settingsInitialState, 13 | locale: browserLanguage(window.navigator.userLanguage || window.navigator.language), 14 | }, 15 | } 16 | const store = createStore(initialState) 17 | 18 | export default class Root extends Component { 19 | 20 | render () { 21 | return ( 22 | 23 | 24 | 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/actions/clientActions.js: -------------------------------------------------------------------------------- 1 | import { CLIENT_CHANGE } from '../constants/actionTypes' 2 | import { 3 | SMALL_SCREEN, SMALL_SCREEN_BREAKPOINT, MEDIUM_SCREEN, 4 | MEDIUM_SCREEN_BREAKPOINT, LARGE_SCREEN, 5 | } from '../constants/screenSizes' 6 | 7 | function isSmallScreen () { 8 | return global.window.innerWidth < SMALL_SCREEN_BREAKPOINT 9 | } 10 | 11 | function isMediumScreen () { 12 | return global.window.innerWidth < MEDIUM_SCREEN_BREAKPOINT 13 | } 14 | 15 | function clientChange (screenSize) { 16 | return { 17 | type: CLIENT_CHANGE, 18 | payload: { screenSize }, 19 | } 20 | } 21 | 22 | export function detectScreenSize () { 23 | return function windowResizeThunk (dispatch, getState) { 24 | const {client} = getState() 25 | 26 | let screenSize = LARGE_SCREEN 27 | if (isSmallScreen()) { 28 | screenSize = SMALL_SCREEN 29 | } else if (isMediumScreen()) { 30 | screenSize = MEDIUM_SCREEN 31 | } 32 | 33 | if (client.screenSize !== screenSize) { 34 | dispatch(clientChange(screenSize)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/actions/dataActions.js: -------------------------------------------------------------------------------- 1 | import { isoWeekRange } from '../date' 2 | import { invertLinkNames } from '../dataManipulation' 3 | import { 4 | EVENTS_LOAD_STARTED, 5 | EVENTS_LOAD_COMPLETED, 6 | EVENTS_LOAD_FAILED, 7 | DATA_ERROR_HIDE, 8 | } from '../constants/actionTypes' 9 | 10 | function startEventsRequest () { 11 | return { 12 | type: EVENTS_LOAD_STARTED, 13 | } 14 | } 15 | 16 | function receiveEvents (events, linkNames) { 17 | return { 18 | type: EVENTS_LOAD_COMPLETED, 19 | payload: { 20 | events, 21 | linkNames, 22 | }, 23 | } 24 | } 25 | 26 | function receiveError (errorObject) { 27 | return { 28 | type: EVENTS_LOAD_FAILED, 29 | payload: errorObject, 30 | } 31 | } 32 | 33 | export function fetchEvents (dataCallback, calendar) { 34 | const {id: calendarId, type: calendarType, date} = calendar 35 | const [dateFrom, dateTo] = isoWeekRange(date) 36 | 37 | return function (dispatch) { 38 | // First dispatch: inform state that loading is going on 39 | dispatch(startEventsRequest()) 40 | 41 | dataCallback({calendarId, calendarType, dateFrom, dateTo}, function (error, result) { 42 | if (error) { 43 | return dispatch(receiveError(error)) 44 | } 45 | const { events, linkNames } = result 46 | // We have (hopefully) received data by now 47 | // FIXME: add error handling 48 | dispatch(receiveEvents(events, invertLinkNames(linkNames))) 49 | }) 50 | } 51 | } 52 | 53 | export function hideDataError () { 54 | return { 55 | type: DATA_ERROR_HIDE, 56 | payload: {}, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/filterActions.js: -------------------------------------------------------------------------------- 1 | import { DISPLAY_FILTERS_CHANGE } from '../constants/actionTypes' 2 | 3 | export function changeDisplayFilters (payload) { 4 | return { 5 | type: DISPLAY_FILTERS_CHANGE, 6 | displayFilters: payload, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/actions/linkActions.js: -------------------------------------------------------------------------------- 1 | import { pushState } from 'redux-router' 2 | 3 | const ENTITY_TYPE_PLURAL = { 4 | course: 'courses', 5 | person: 'people', 6 | room: 'rooms', 7 | } 8 | 9 | function entityType (type) { 10 | return ENTITY_TYPE_PLURAL[type] || type 11 | } 12 | 13 | export function calendarUrl ({id, date, type = null}) { 14 | const urlType = entityType(type) 15 | const dateQuery = date ? `?date=${date}` : '' 16 | 17 | let path = `${urlType}/${id}` 18 | if (urlType === 'people' && id === 'me') { 19 | path = '' 20 | } 21 | 22 | return `${path}${dateQuery}` 23 | } 24 | 25 | export function changeCalendar (calendar) { 26 | // XXX: state object is required by history 27 | // https://github.com/rackt/history/blob/master/docs/GettingStarted.md#navigation 28 | return pushState(null, calendarUrl(calendar)) 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/searchActions.js: -------------------------------------------------------------------------------- 1 | import { SEARCH_REQUEST, SEARCH_RESPONSE, SEARCH_CLEAR } from '../constants/actionTypes' 2 | 3 | function startSearchRequest (query) { 4 | return { 5 | type: SEARCH_REQUEST, 6 | payload: { query }, 7 | } 8 | } 9 | 10 | function receiveSearchResults (results) { 11 | return { 12 | type: SEARCH_RESPONSE, 13 | payload: { results }, 14 | } 15 | } 16 | 17 | export function clearSearchResults () { 18 | return { 19 | type: SEARCH_CLEAR, 20 | } 21 | } 22 | 23 | export function fetchSearchResults (searchCallback, query) { 24 | return function searchResultsDispatcher (dispatch) { 25 | if (!query) { 26 | return dispatch(receiveSearchResults([])) 27 | } 28 | // First dispatch: inform state that request has been sent 29 | dispatch(startSearchRequest(query)) 30 | 31 | searchCallback(query, results => { 32 | // FIXME: add error handling 33 | dispatch(receiveSearchResults(results)) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/actions/semesterActions.js: -------------------------------------------------------------------------------- 1 | import { SEMESTER_LOAD_COMPLETED } from '../constants/actionTypes' 2 | import { findSemester, convertRawSemester, dateInSemester } from '../semester' 3 | 4 | function receiveSemesterData (semester) { 5 | return { 6 | type: SEMESTER_LOAD_COMPLETED, 7 | payload: semester, 8 | } 9 | } 10 | 11 | export function invalidateSemesterData (semester) { 12 | return { 13 | ...semester, 14 | valid: false, 15 | } 16 | } 17 | 18 | export function fetchSemesterData (semesterCallback, date) { 19 | return function semesterDataThunk (dispatch, getState) { 20 | const {semester} = getState() 21 | if (semester && semester.valid && dateInSemester(semester, date)) { 22 | return 23 | } 24 | 25 | semesterCallback(data => { 26 | if (!data) { 27 | dispatch(receiveSemesterData(invalidateSemesterData(semester))) 28 | return 29 | } 30 | 31 | const currentSemester = findSemester(data, date) 32 | if (!currentSemester) { 33 | dispatch(receiveSemesterData(invalidateSemesterData(semester))) 34 | return 35 | } 36 | 37 | dispatch(receiveSemesterData(convertRawSemester(currentSemester))) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/actions/settingsActions.js: -------------------------------------------------------------------------------- 1 | import { SETTINGS_CHANGE } from '../constants/actionTypes' 2 | 3 | export function changeSettings (payload) { 4 | return { 5 | type: SETTINGS_CHANGE, 6 | settings: payload, 7 | } 8 | } 9 | 10 | export const setLocale = (newLocale) => changeSettings({locale: newLocale}) 11 | export const setLayout = (layout) => changeSettings({layout: layout}) 12 | export const setEventsColors = (enabled) => changeSettings({eventsColors: enabled}) 13 | export const setFullWeek = (enabled) => changeSettings({fullWeek: enabled}) 14 | export const setFacultyGrid = (enabled) => changeSettings({facultyGrid: enabled}) 15 | -------------------------------------------------------------------------------- /src/actions/uiActions.js: -------------------------------------------------------------------------------- 1 | import { SIDEBAR_DISPLAY, EVENT_DISPLAY } from '../constants/actionTypes' 2 | 3 | export function displaySidebar (sidebar) { 4 | return { 5 | type: SIDEBAR_DISPLAY, 6 | payload: { sidebar }, 7 | } 8 | } 9 | 10 | export function displayEvent (eventId) { 11 | return { 12 | type: EVENT_DISPLAY, 13 | payload: { eventId }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import { USER_LOAD_STARTED, USER_LOAD_COMPLETED } from '../constants/actionTypes' 2 | import { fetchUserCallback, logoutUserCallback } from '../callbacks' 3 | 4 | function startUserRequest () { 5 | return { 6 | type: USER_LOAD_STARTED, 7 | } 8 | } 9 | 10 | function receiveUser (payload) { 11 | return { 12 | type: USER_LOAD_COMPLETED, 13 | payload, 14 | } 15 | } 16 | 17 | export function fetchUserData () { 18 | return function thunk (dispatch) { 19 | dispatch(startUserRequest()) 20 | 21 | fetchUserCallback((error, result) => { 22 | if (error) { 23 | return dispatch(receiveUser(null)) 24 | } 25 | dispatch(receiveUser(result)) 26 | }) 27 | } 28 | } 29 | 30 | export function logoutUser () { 31 | return function thunk () { 32 | logoutUserCallback(error => { 33 | if (error) { 34 | console.error(error) 35 | global.alert(error.message) 36 | } else { 37 | global.location.href = 'landing.html' 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import Raven from 'raven-js' 2 | 3 | if (process.env.SENTRY_DSN && process.env.NODE_ENV === 'production') { 4 | Raven.config(process.env.SENTRY_DSN).install() 5 | } 6 | 7 | import React from 'react' 8 | import ReactDOM from 'react-dom' 9 | 10 | import Counterpart from 'counterpart' 11 | 12 | import 'moment/locale/cs' 13 | import LocaleCS from './locales/cs.json' 14 | import LocaleEN from './locales/en.json' 15 | 16 | import Root from './Root' 17 | 18 | const rootElement = document.getElementById('fittable') 19 | 20 | // Register translations 21 | Counterpart.registerTranslations('en', LocaleEN) 22 | Counterpart.registerTranslations('cs', Object.assign(LocaleCS, { 23 | counterpart: { 24 | pluralize: (entry, count) => { 25 | entry[ (count === 0 && 'zero' in entry) ? 'zero' : (count === 1) ? 'one' : 'other' ] 26 | }, 27 | }, 28 | })) 29 | // Counterpart.setLocale(options.locale) 30 | // Moment.locale(options.locale) 31 | 32 | ReactDOM.render(, rootElement) 33 | -------------------------------------------------------------------------------- /src/callbacks/index.js: -------------------------------------------------------------------------------- 1 | // XXX: CommonJS for conditional import 2 | if (process.env.FITTABLE_SOURCE === 'sirius') { 3 | module.exports = require('./sirius') 4 | } else { 5 | module.exports = require('./faux') 6 | } 7 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { is } from 'ramda' 2 | 3 | /** 4 | * Detects if there is an information about browser language. 5 | * If not, returns 'cs'. If there is the information and the language 6 | * string starts with 'cs', returns 'cs', otherwise 'en'. 7 | */ 8 | export function browserLanguage (navigatorLanguage = 'cs') { 9 | if (is(String, navigatorLanguage) && navigatorLanguage.toLowerCase().substr(0, 2) === 'cs') { 10 | return 'cs' 11 | } 12 | return 'en' 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Controls.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Wraps all controls displayed in the heading of widget. 3 | * Contains week controllers and functions tools. 4 | */ 5 | 6 | import React, { PropTypes } from 'react' 7 | import { isScreenMediumAndUp } from '../screen' 8 | 9 | import SemesterWeek from './SemesterWeek' 10 | import WeekNav from './WeekNav' 11 | import FunctionsBar from './FunctionsBar' 12 | import WeekSwitcher from './WeekSwitcher' 13 | import { shiftDate } from '../date' 14 | import { semester as semesterType } from '../constants/propTypes' 15 | 16 | const propTypes = { 17 | onDateChange: PropTypes.func.isRequired, 18 | onSettingsPanelChange: PropTypes.func.isRequired, 19 | viewDate: React.PropTypes.instanceOf(Date), 20 | days7: PropTypes.bool, 21 | semester: semesterType, 22 | } 23 | 24 | class Controls extends React.Component { 25 | 26 | /** 27 | * Handles WeekNav's previous button click event 28 | */ 29 | handlePrevClick () { 30 | const shiftFun = shiftDate(this.props.viewDate) 31 | const skipWeekend = !this.props.days7 && this.props.viewDate.getDay() === 1 32 | 33 | let shiftBy 34 | 35 | if (isScreenMediumAndUp(this.props.screenSize)) { 36 | shiftBy = shiftFun('week', -1) 37 | } else { 38 | shiftBy = shiftFun('day', skipWeekend ? -3 : -1) 39 | } 40 | this.props.onDateChange(shiftBy) 41 | } 42 | 43 | /** 44 | * Handles WeekNav's next button click event 45 | */ 46 | handleNextClick () { 47 | const shiftFun = shiftDate(this.props.viewDate) 48 | const skipWeekend = !this.props.days7 && this.props.viewDate.getDay() === 5 49 | 50 | let shiftBy 51 | 52 | if (isScreenMediumAndUp(this.props.screenSize)) { 53 | shiftBy = shiftFun('week', +1) 54 | } else { 55 | shiftBy = shiftFun('day', skipWeekend ? +3 : +1) 56 | } 57 | this.props.onDateChange(shiftBy) 58 | } 59 | 60 | /** 61 | * Handles a click on a week row in the WeekSelector 62 | */ 63 | handleWeekClick () { 64 | this.refs.weekSwitcher.toggle() 65 | } 66 | 67 | render () { 68 | 69 | return ( 70 |
71 | 78 | 84 | 88 | 89 |
90 | ) 91 | } 92 | } 93 | 94 | Controls.propTypes = propTypes 95 | 96 | export default Controls 97 | -------------------------------------------------------------------------------- /src/components/Day.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component representing one row (day) in timetable. 3 | */ 4 | 5 | import React, { PropTypes } from 'react' 6 | import moment from 'moment' 7 | 8 | const propTypes = { 9 | date: PropTypes.instanceOf(Date), 10 | viewDate: PropTypes.instanceOf(Date), 11 | } 12 | 13 | class Day extends React.Component { 14 | 15 | render () { 16 | const date = moment(this.props.date) 17 | const isToday = date.isSame(moment(), 'days') 18 | const isSelected = date.isSame(this.props.viewDate, 'days') 19 | const className = `day ${isToday ? 'active' : ''} ${isSelected ? 'selected' : ''}` 20 | 21 | return ( 22 |
23 |
24 |
25 | {date.date()} 26 | {date.format('dddd')} 27 |
28 |
29 |
30 | {this.props.children} 31 |
32 |
33 | ) 34 | } 35 | } 36 | 37 | Day.propTypes = propTypes 38 | 39 | export default Day 40 | -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CP from 'counterpart' 3 | 4 | import LogoutButton from './LogoutButton' 5 | 6 | export default function Footer ({ userName, onLogout }) { 7 | return ( 8 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/FunctionFilter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import R from 'ramda' 3 | import CP from 'counterpart' 4 | 5 | // Types of filterable events. 6 | const eventTypes = [ 7 | 'lecture', 'tutorial', 'laboratory', 'exam', 'assessment', 'course_event', 8 | 'teacher_timetable_slot', 'other', 9 | ] 10 | 11 | /** 12 | * Provides ability to filter out some types of events from timetable. 13 | * 14 | * @param {Object} options.displayFilter State of the filters. 15 | * @param {Function} options.onFilterChange Function to be called on a filter change. 16 | */ 17 | function FunctionFilter ({ displayFilter, onFilterChange }) { 18 | 19 | // Handles click on a filter button. 20 | function handleToggleFilter (filterName) { 21 | const oldState = displayFilter[filterName] 22 | onFilterChange({ [filterName]: !oldState }) 23 | } 24 | 25 | const filterButton = (filterName) => ( 26 |
  • handleToggleFilter(filterName) } 30 | > 31 | { CP.translate('event_type.' + filterName) } 32 |
  • 33 | ) 34 | 35 | return ( 36 |
    37 |
    38 |

    { CP.translate('functions.filter.heading') }

    39 |
      40 | { R.map(filterButton, eventTypes) } 41 |
    42 |
    43 |
    44 | ) 45 | } 46 | 47 | export default FunctionFilter 48 | -------------------------------------------------------------------------------- /src/components/FunctionsBar.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component wrapping all function control buttons located in upper right corner of the widget. 3 | */ 4 | 5 | import React, { PropTypes } from 'react' 6 | import CP from 'counterpart' 7 | 8 | const propTypes = { 9 | onPanelToggle: PropTypes.func, 10 | } 11 | 12 | class FunctionsBar extends React.Component { 13 | 14 | /** 15 | * Handles a click on the search icon 16 | */ 17 | handleSearchClick () { 18 | this.props.onPanelToggle('search') 19 | 20 | // Focus the search input 21 | setTimeout(() => { 22 | document.getElementById('searchinput').focus() 23 | }, 1000) 24 | } 25 | 26 | onFunctionClick (functionName) { 27 | this.props.onPanelToggle(functionName) 28 | } 29 | 30 | render () { 31 | return ( 32 |
    33 | 41 | 49 | 58 |
    59 | 60 | ) 61 | } 62 | } 63 | 64 | FunctionsBar.propTypes = propTypes 65 | 66 | export default FunctionsBar 67 | -------------------------------------------------------------------------------- /src/components/FunctionsSidebar.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component wrapping function panels 3 | */ 4 | 5 | import React, { PropTypes } from 'react' 6 | 7 | import { options as optionsType } from '../constants/propTypes' 8 | import FunctionSettings from './FunctionSettings' 9 | import FunctionFilter from './FunctionFilter' 10 | import SidebarIcal from './SidebarIcal' 11 | 12 | const propTypes = { 13 | opened: PropTypes.oneOf(['settings', 'search', 'filter', 'ical']), 14 | onSettingsChange: PropTypes.func, 15 | settings: PropTypes.shape(optionsType), 16 | displayFilter: PropTypes.objectOf(PropTypes.bool), // FIXME: shared type 17 | onFilterChange: PropTypes.func, 18 | user: PropTypes.shape({ 19 | id: PropTypes.string, 20 | name: PropTypes.string, 21 | publicAccessToken: PropTypes.string, 22 | }), 23 | } 24 | 25 | class FunctionsSidebar extends React.Component { 26 | 27 | render () { 28 | let functionToRender 29 | 30 | if (this.props.opened === 'settings') { 31 | functionToRender = ( 32 | 37 | ) 38 | } 39 | if (this.props.opened === 'filter') { 40 | functionToRender = ( 41 | 45 | ) 46 | } 47 | 48 | if (this.props.opened === 'ical') { 49 | const {id, publicAccessToken} = this.props.user 50 | functionToRender = ( 51 | 52 | ) 53 | } 54 | 55 | const className = `functions-sidebar ${this.props.opened !== null ? '' : 'hide'}` 56 | 57 | return ( 58 |
    59 |
    60 | {functionToRender} 61 |
    62 |
    63 | ) 64 | } 65 | } 66 | 67 | FunctionsSidebar.propTypes = propTypes 68 | 69 | export default FunctionsSidebar 70 | -------------------------------------------------------------------------------- /src/components/Grid.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Component for drawing timetable's grid using JS-generated SVG image 3 | */ 4 | 5 | import React, { PropTypes } from 'react' 6 | import R from 'ramda' 7 | 8 | const propTypes = { 9 | horizontal: PropTypes.bool, 10 | hours: PropTypes.number.isRequired, 11 | offset: PropTypes.number.isRequired, 12 | color: PropTypes.string.isRequired, 13 | } 14 | 15 | class Grid extends React.Component { 16 | 17 | createLine (type, hours, offset, color) { 18 | const defaults = { 19 | className: 'Grid-line', 20 | x1: '0', 21 | y1: '0', 22 | x2: '100%', 23 | y2: '100%', 24 | stroke: color, 25 | strokeWidth: 1, 26 | } 27 | 28 | return (n) => { 29 | const pos = ((n + offset) / hours * 100 + 0.1) + '%' 30 | // Position percentage +0.1% to avoid cropping the first line 31 | 32 | const inheritedParams = { 33 | ...defaults, 34 | key: n, 35 | } 36 | 37 | if (type === 'vertical') { 38 | return 39 | } else { 40 | return 41 | } 42 | } 43 | } 44 | 45 | getLines () { 46 | const hours = Math.ceil(this.props.hours) 47 | const type = this.props.horizontal ? 'horizontal' : 'vertical' 48 | 49 | return R.times( 50 | this.createLine(type, this.props.hours, this.props.offset, this.props.color), 51 | hours 52 | ) 53 | } 54 | 55 | render () { 56 | return ( 57 | 58 | {this.getLines(this)} 59 | 60 | ) 61 | } 62 | 63 | } 64 | 65 | Grid.propTypes = propTypes 66 | 67 | export default Grid 68 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CP from 'counterpart' 3 | 4 | function calendarHeader ({type, id}) { 5 | if (type === 'people' && id === 'me') { 6 | return `${CP.translate('calendarType.personal')}` 7 | } 8 | 9 | const cpKey = `calendarType.${type}` 10 | return `${CP.translate(cpKey)} ${id}` 11 | } 12 | 13 | export default function Header ({calendar, semesterName, children}) { 14 | return ( 15 |
    16 | {children} 17 | 18 |

    {calendarHeader(calendar)}

    19 |
    {semesterName}
    20 |
    21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/HourLabel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function HourLabel ({position, length, layout, children}) { 4 | let style 5 | if (layout === 'horizontal') { 6 | style = { 7 | left: position * 100 + '%', 8 | width: length * 100 + '%', 9 | } 10 | } else { 11 | style = { 12 | top: position * 100 + '%', 13 | height: length * 100 + '%', 14 | } 15 | } 16 | 17 | return ( 18 |
    19 | {children} 20 |
    21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/LogoutButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CP from 'counterpart' 3 | 4 | export default function LogoutButton ({ onClick }) { 5 | return ( 6 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/NowIndicator.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Indicator showing line representing actual point of today's time 3 | */ 4 | 5 | import React from 'react' 6 | import moment from 'moment' 7 | import { isScreenMediumAndUp } from '../screen' 8 | import { weekdayNum } from '../date' 9 | 10 | function NowIndicator ({ 11 | currentDate, timeline, viewDate, days7, screenSize, horizontalLayout }) { 12 | 13 | const now = moment(currentDate) 14 | 15 | const startOfToday = now.clone().startOf('day').add(timeline.start, 'seconds') 16 | // Distance from start of timeline in seconds 17 | const nowSeconds = now.diff(startOfToday, 'seconds') 18 | 19 | const dayWidth = 1 / (days7 ? 7 : 5) 20 | const length = nowSeconds / timeline.duration 21 | const currentWeekday = now.isoWeekday() - 1 22 | const offset = currentWeekday * dayWidth 23 | const displayMultipleDays = isScreenMediumAndUp(screenSize) 24 | 25 | const selectedDay = weekdayNum(viewDate) 26 | 27 | // Indicator will be shown if: 28 | // - it is not bleeding out of the timeline (i.e. length and offset are within some percentage) 29 | // - we are displaying current week 30 | let shown = (length > 0 && length < 1 && offset < 1) && 31 | now.isSame(viewDate, 'isoWeek') 32 | 33 | if (displayMultipleDays && currentWeekday !== selectedDay) { 34 | shown = false 35 | } 36 | 37 | if (!shown) { 38 | // XXX: stateless components cannot return null or false (for now) 39 | return