├── .dockerignore ├── .env ├── .github ├── ISSUE_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── report-a-problem.md ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .yarnrc.yml ├── CNAME ├── Dockerfile ├── LICENSE ├── README.md ├── _config.yml ├── docker-compose.yml ├── package.json ├── public ├── 404.html ├── icons │ ├── classclockicon-192.png │ ├── classclockicon-512.png │ └── favicon.ico ├── index.html └── manifest.json ├── robots.txt ├── scripts └── vercel-build.sh ├── src ├── @types │ ├── bellschedule.test.ts │ ├── bellschedule.ts │ ├── classperiod.test.ts │ ├── classperiod.ts │ ├── redux-first-routing.d.ts │ ├── school.test.ts │ ├── school.ts │ ├── time.test.ts │ ├── time.ts │ ├── updateTimestampedObject.test.ts │ └── updateTimestampedObject.ts ├── components │ ├── Block │ │ ├── Block.css │ │ ├── Block.test.tsx │ │ ├── Block.tsx │ │ └── __snapshots__ │ │ │ └── Block.test.tsx.snap │ ├── BlockLink.tsx │ ├── Icon.test.tsx │ ├── Icon.tsx │ ├── Link.test.tsx │ ├── Link.tsx │ ├── List │ │ └── List.tsx │ ├── ScheduleEntry │ │ └── ScheduleEntry.tsx │ ├── SelectionList │ │ ├── SelectionList.css │ │ └── SelectionList.tsx │ ├── SocialIcons.tsx │ ├── StatusIndicator.tsx │ ├── StopLight.tsx │ └── __snapshots__ │ │ ├── Icon.test.tsx.snap │ │ └── Link.test.tsx.snap ├── global.css ├── index.css ├── index.tsx ├── package.alias.json ├── pages │ ├── Admin │ │ ├── AdminPage.tsx │ │ ├── CalendarDates.tsx │ │ ├── LoginRedirect.tsx │ │ ├── authProvider.ts │ │ └── resources.tsx │ ├── App.test.tsx │ ├── App.tsx │ ├── Schedule.test.tsx │ ├── Schedule.tsx │ ├── SchoolSelect.tsx │ ├── Settings │ │ ├── Settings.test.tsx │ │ ├── Settings.tsx │ │ ├── __snapshots__ │ │ │ └── Settings.test.tsx.snap │ │ └── settings.css │ ├── Welcome.tsx │ ├── __snapshots__ │ │ └── Schedule.test.tsx.snap │ └── errors │ │ ├── PageNotFound.tsx │ │ └── ServerError.tsx ├── react-app-env.d.ts ├── serviceWorker.ts ├── services │ ├── auth0-provider-with-history.tsx │ ├── classclock-dataprovider.ts │ ├── classclock.test.ts │ └── classclock.ts ├── store │ ├── schools │ │ ├── actions.test.ts │ │ ├── actions.ts │ │ ├── reducer.test.ts │ │ ├── reducer.ts │ │ └── types.ts │ ├── store.ts │ └── usersettings │ │ ├── actions.ts │ │ ├── reducer.ts │ │ └── types.ts └── utils │ ├── IPageInterface.ts │ ├── constants.ts │ ├── enums.ts │ ├── errors.ts │ ├── helpers.test.ts │ ├── helpers.tsx │ ├── routes.tsx │ ├── testconstants.ts │ └── typetransform.ts ├── tsconfig.json ├── tslint.json ├── vercel.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | coverage/ 4 | 5 | .dockerignore 6 | Dockerfile 7 | 8 | .gitignore 9 | .env 10 | .eslintrc 11 | .editorconfig 12 | .gitlab-ci.yml 13 | 14 | _site/ 15 | npm-debug.log 16 | Dockerfile* 17 | docker-compose* 18 | .dockerignore 19 | .git 20 | .gitignore 21 | README.md 22 | LICENSE 23 | .vscode -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_VERSION=$npm_package_version 2 | REACT_APP_NAME=$npm_package_name 3 | REACT_APP_AUTHORNAME=$npm_package_author_name 4 | REACT_APP_AUTHORURL=$npm_package_author_url 5 | REACT_APP_CONTRIBUTORS=$npm_package_contributors 6 | # REACT_APP_SENTRY_DSN=YOUR_DSN_HERE -------------------------------------------------------------------------------- /.github/ ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | I am submitting a... 2 | 3 | - [ ] Report of a problem with the site 4 | - [ ] Request for a new feature to add to the site (pease make sure someone else hasnt already requested it) 5 | - [ ] Question about how to use the site (have you tried looking at the README or the Wiki?) 6 | - [ ] Something else 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Feature Request 4 | url: https://github.com/MoralCode/ClassClock/discussions 5 | about: Please create feature and support requests in the Discussions tab. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/report-a-problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a Problem 3 | about: Report an issue with the site 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | 4 | 5 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 6 | .yarn/* 7 | !.yarn/cache 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/sdks 12 | !.yarn/versions 13 | 14 | node_modules 15 | yarn-error.log 16 | 17 | .DS_Store 18 | .history 19 | 20 | junit.xml 21 | 22 | # This file is generated by the Smart Merge workflow 23 | /merge-conflict-resolution.patch 24 | 25 | # The tarballs generated by "yarn pack" are never kept either 26 | package.tgz 27 | 28 | # The artifacts generated by "yarn release:all" are never kept 29 | /artifacts 30 | /dist 31 | 32 | # Our documentation is now handled by Netlify 33 | /docs 34 | 35 | # Those files are meant to be local-only 36 | /packages/*/bundles 37 | 38 | # Those folders are meant to contain the prepack build artifacts; we don't commit them 39 | /packages/*/lib/* 40 | 41 | # Those packages are built inline and the artifacts must not be checked-in 42 | /packages/yarnpkg-libui/sources/**/*.js 43 | /packages/yarnpkg-libui/sources/**/*.d.ts 44 | 45 | # Used by /scripts/stable-versions-store.js 46 | /scripts/stable-versions-store.json 47 | 48 | /vscode-case-study 49 | 50 | .idea 51 | 52 | coverage 53 | 54 | 55 | # dependencies 56 | /.pnp 57 | .pnp.js 58 | 59 | 60 | # production 61 | /build 62 | 63 | # misc 64 | .DS_Store 65 | .env.local 66 | .env.development.local 67 | .env.test.local 68 | .env.production.local 69 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "tabWidth": 4 4 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | - 13 6 | 7 | matrix: 8 | allow_failures: 9 | - node_js: 10 10 | - node_js: 13 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | web.classclock.app -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine3.11 2 | 3 | WORKDIR /classclock 4 | 5 | ADD package.json yarn.lock /classclock/ 6 | RUN yarn install 7 | COPY . /classclock/ 8 | 9 | CMD yarn run start:prod 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![ClassClock Icon](./public/icons/classclockicon-512.png) 2 | 3 | # ClassClock 4 | 5 | [![Join the chat at https://gitter.im/MoralCode/ClassClock](https://badges.gitter.im/MoralCode/ClassClock.svg)](https://gitter.im/MoralCode/ClassClock?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | 8 | ## What is ClassClock? 9 | ClassClock is a web app to give students and teachers easy access to: 10 | - current date and time 11 | - daily schedules (You are on the Mon/Tues Schedule) 12 | - the class period you are currently in (i.e. 2nd Period) 13 | - a countdown until class is over 14 | - the generic name of your next class (i.e. 3rd Period) 15 | 16 | ClassClock is useful for: 17 | - **Teachers** - to check how much teaching time is left in class 18 | - **Students** - to remind themselves of where they need to be and when 19 | - **Parents** - to see the optimal time to contact their student without interfering with instruction time 20 | - **Everyone** - to have quick and easy access to varied schedules that may otherwise be complicated, confusing or hard to remember 21 | 22 | 23 | ### Upcoming Features 24 | Here are some features to look forward to in a future version: 25 | 26 | - [ ] Support for multiple schools (currently only supports one) 27 | - [ ] Detection of block (A/B) schedules 28 | - [ ] Countdown to school breaks (winter break, spriing break, summer .etc) 29 | - [ ] Support for custom schedules (assemblies, parades, combined schedules etc.) 30 | - [ ] The ability to change what is displayed 31 | - [ ] The ability for students to upload their schedules to get more detailed information such as teacher name and room number 32 | 33 | 34 | 35 | 36 | ## How to install? 37 | 38 | ClassClock is a web-based app that can be saved to your phone's home screen or bookmarked in your browser for easy access. 39 | 40 | ### iOS 41 | 42 | 1. Navigate to ClassClock in Safari (not tested on other browsers) 43 | 2. Click the "Share" or "Action" button (it looks like a square with an arrow pointing up out of the top) 44 | 3. Click "Add to Home Screen" on the bottom row (you might have to scroll sideways to see it) 45 | 4. Click "Done" in the top corner of the screen to add it to your home screen. 46 | 47 | 48 | ### Android 49 | 50 | 1. Navigate to ClassClock (instructions for Google Chrome) 51 | 2. Click the "Overflow" button on the top right of your screen (it looks like three vertical dots) 52 | 3. Click "Add to Home Screen" button in the menu (it's about 2/3 of the way down) 53 | 4. Click "Add" in the dialog box that pops up to add it to your home screen. 54 | 55 | 56 | 57 | ## Developer setup 58 | 59 | ### Environment Variables 60 | 61 | REACT_APP_AUTH0_DOMAIN - the domain for the auth0 tenant to use (provided in application details) 62 | 63 | REACT_APP_AUTH0_CLIENT_ID - the client ID for the auth0 application to connect to 64 | 65 | REACT_APP_AUTH0_AUDIENCE - the auth0 audience (probably your API URL) to use 66 | 67 | REACT_APP_SENTRY_DSN - if you want to use sentry for monititoring errors, provide your DSN value from the sentry app so ClassClock knows where to send error information 68 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | 2 | name: ClassClock 3 | githubURL: https://github.com/MoralCode/ClassClock 4 | slackURL: https://join.slack.com/t/classclock/shared_invite/enQtNTE0MDkyNzAwNzU3LWNhMGUwODU2ZjhkYTYxMTgzNDE1OWEyMGY2OGNiNTBhOWM5NDVhZGUzNDVlNzRiZTE3NTNmODFjYWNkNDhmMDU 5 | twitterURL: https://twitter.com/classclockapp 6 | instagramURL: https://www.instagram.com/classclockapp/ -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | mariadb: 4 | image: mariadb 5 | ports: 6 | - 3307:3306 7 | environment: 8 | - MARIADB_ROOT_PASSWORD=[PASSWORD_HERE] 9 | volumes: 10 | - 'classclock_db:/var/lib/mysql' 11 | classclock-api: 12 | image: classclock-api 13 | ports: 14 | - '8686:8000' 15 | environment: 16 | - DB_USERNAME=username 17 | - DB_PASSWORD=password 18 | - DB_HOST=mariadb 19 | - DB_NAME=classclock 20 | depends_on: 21 | - mariadb 22 | # classclock: 23 | # image: classclock-web 24 | # ports: 25 | # - '3030:3000' 26 | # environment: 27 | # - 28 | # depends_on: 29 | # - classclock-api 30 | 31 | volumes: 32 | classclock_db: 33 | driver: local -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "classclock", 3 | "version": "0.4.3", 4 | "homepage": "https://web.classclock.app", 5 | "author": { 6 | "name": "Adrian Edwards", 7 | "email": "adrian@adriancedwards.com", 8 | "url": "https://www.adriancedwards.com" 9 | }, 10 | "contributors": [ 11 | { 12 | "name": "Nick DeGroot", 13 | "url": "https://nickthegroot.com/" 14 | } 15 | ], 16 | "repository": "github:MoralCode/ClassClock", 17 | "private": true, 18 | "dependencies": { 19 | "@auth0/auth0-react": "^1.9.0", 20 | "@fortawesome/fontawesome-svg-core": "^6.6.0", 21 | "@fortawesome/free-brands-svg-icons": "^6.6.0", 22 | "@fortawesome/free-regular-svg-icons": "^6.6.0", 23 | "@fortawesome/free-solid-svg-icons": "^6.1.2", 24 | "@fortawesome/react-fontawesome": "^0.2.2", 25 | "@fullcalendar/core": "^5.11.0", 26 | "@fullcalendar/daygrid": "^5.11.0", 27 | "@fullcalendar/interaction": "^5.11.0", 28 | "@fullcalendar/react": "^5.11.1", 29 | "@sentry/react": "^7.37.1", 30 | "@sentry/tracing": "^7.37.1", 31 | "date-fns": "^1.30.1", 32 | "lodash.find": "^4.6.0", 33 | "luxon": "^1.25.0", 34 | "react": "^16.8.6", 35 | "react-admin": "^4.2.2", 36 | "react-dom": "^16.8.6", 37 | "react-redux": "^7.1.0", 38 | "react-scripts": "^5.0.0", 39 | "redux": "^4.0.1", 40 | "redux-first-routing": "^0.3.0", 41 | "redux-persist": "^5.10.0", 42 | "redux-thunk": "^2.3.0", 43 | "ts-jest": "^28.0.7", 44 | "typescript": "^3.6.4", 45 | "universal-router": "^8.2.0" 46 | }, 47 | "scripts": { 48 | "start": "react-scripts start", 49 | "start:prod": "react-scripts build; serve -l tcp://0.0.0.0:3000 -s build", 50 | "build": "react-scripts build", 51 | "test": "react-scripts test --verbose=true", 52 | "eject": "react-scripts eject" 53 | }, 54 | "eslintConfig": { 55 | "extends": "react-app" 56 | }, 57 | "browserslist": { 58 | "production": [ 59 | ">0.2%", 60 | "not dead", 61 | "not op_mini all" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 safari version" 67 | ] 68 | }, 69 | "devDependencies": { 70 | "@types/fetch-mock": "^7.3.2", 71 | "@types/jest": "^28.1.6", 72 | "@types/lodash.find": "^4.6.6", 73 | "@types/luxon": "^1.25.0", 74 | "@types/node": "^12.7.3", 75 | "@types/react": "^16.9.2", 76 | "@types/react-dom": "^16.9.0", 77 | "@types/react-redux": "^7.1.0", 78 | "@types/react-test-renderer": "^16.9.0", 79 | "@types/redux-logger": "^3.0.7", 80 | "@types/redux-mock-store": "^1.0.3", 81 | "fetch-mock": "^8.1.0", 82 | "mockdate": "^2.0.5", 83 | "prettier": "1.18.2", 84 | "react-test-renderer": "16.8.6", 85 | "redux-logger": "^3.0.6", 86 | "redux-mock-store": "^1.5.4", 87 | "tslint": "^5.18.0", 88 | "tslint-config-prettier": "^1.18.0", 89 | "tslint-react": "^4.0.0" 90 | }, 91 | "resolutions": { 92 | "@types/react": "^16.9.2", 93 | "@types/react-dom": "^16.9.2" 94 | }, 95 | "packageManager": "yarn@4.5.0" 96 | } 97 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/icons/classclockicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoralCode/ClassClock/935beb1a6e26d4189f4e3a409dab625031c047f6/public/icons/classclockicon-192.png -------------------------------------------------------------------------------- /public/icons/classclockicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoralCode/ClassClock/935beb1a6e26d4189f4e3a409dab625031c047f6/public/icons/classclockicon-512.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoralCode/ClassClock/935beb1a6e26d4189f4e3a409dab625031c047f6/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | ClassClock 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "ClassClock", 3 | "name": "ClassClock", 4 | "icons": [ 5 | { 6 | "src": "/icons/classclockicon-192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "/icons/classclockicon-512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "src": "/icons/favicon.ico", 17 | "type": "image/x-icon", 18 | "sizes": "64x64 48x48 32x32 16x16" 19 | } 20 | ], 21 | "start_url": "/", 22 | "background_color": "#b7d3e1", 23 | "display": "standalone", 24 | "scope": "/", 25 | "theme_color": "#b7d3e1" 26 | } -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /scripts/vercel-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://adriel.dev/posts/2024-02-09-speeding-up-builds-for-berry-yarn-projects-on-vercel#conclusion 4 | 5 | echo "Starting the script..." 6 | 7 | yarn set version berry 8 | 9 | # Check if node_modules/install-state.gz exists and copy it to .yarn/install-state.gz 10 | if [ -f "node_modules/install-state.gz" ]; then 11 | echo "Copying node_modules/install-state.gz to .yarn/install-state.gz..." 12 | cp node_modules/install-state.gz .yarn/install-state.gz 13 | else 14 | echo "node_modules/install-state.gz does not exist. Skipping copy." 15 | fi 16 | 17 | # Set an environment variable YARN_CACHE_FOLDER=./.next/cache/yarn 18 | echo "Setting environment variable YARN_CACHE_FOLDER=./.next/cache/yarn..." 19 | export YARN_CACHE_FOLDER=./.next/cache/yarn 20 | 21 | # Run yarn install --immutable 22 | echo "Running 'yarn install --immutable'..." 23 | yarn install --immutable 24 | 25 | # Check if .yarn/install-state.gz exists and copy it to node_modules/install-state.gz 26 | if [ -f ".yarn/install-state.gz" ]; then 27 | echo "Copying .yarn/install-state.gz back to node_modules/install-state.gz..." 28 | cp .yarn/install-state.gz node_modules/install-state.gz 29 | else 30 | echo ".yarn/install-state.gz does not exist. Skipping copy." 31 | fi 32 | 33 | echo "Script completed." -------------------------------------------------------------------------------- /src/@types/bellschedule.test.ts: -------------------------------------------------------------------------------- 1 | import BellSchedule from "./bellschedule"; 2 | import { bellSchedule as schedule, bellScheduleJSON, classPeriod, bellScheduleEndpoint, bellScheduleName, bellScheduleId, startTimeDT, beforeClassDT, duringClassDT, endTimeDT, afterClassDT, bellScheduleClasses, schoolTimezone, betweenClass, classPeriod2, startTime2DT, endTime2DT, duringClass2, afterClass2 } from "../utils/testconstants"; 3 | import { DateTime } from "luxon"; 4 | 5 | describe("BellSchedule", () => { 6 | 7 | it("should get from JSON", () => { 8 | //TODO: the start and end times should be plain HH:MM, not full datetimes 9 | 10 | let constructed = BellSchedule.fromJson(bellScheduleJSON) 11 | expect(constructed.getName()).toEqual(schedule.getName()); 12 | expect(constructed.getColor()).toEqual(schedule.getColor()); 13 | expect(constructed.getAllClasses()).toEqual(schedule.getAllClasses()); 14 | expect(constructed.getDates()).toEqual(schedule.getDates()); 15 | expect(constructed.getDisplayName()).toEqual(schedule.getDisplayName()); 16 | expect(constructed.getEndpoint()).toEqual(schedule.getEndpoint()); 17 | expect(constructed.getIdentifier()).toEqual(schedule.getIdentifier()); 18 | 19 | }); 20 | 21 | it("can get its identifier", () => { 22 | expect(schedule.getIdentifier()).toBe(bellScheduleId); 23 | }); 24 | 25 | it("can get its name", () => { 26 | expect(schedule.getName()).toEqual(bellScheduleName); 27 | }); 28 | 29 | it("can return API endpoint", () => { 30 | expect(schedule.getEndpoint()).toBe(bellScheduleEndpoint); 31 | }); 32 | 33 | 34 | it("can get its dates", () => { 35 | expect(schedule.getDates()).toEqual([ 36 | DateTime.fromISO("2019-07-28T07:37:50.634", {locale: "en-US"}).toUTC(), 37 | DateTime.fromISO("2019-07-29T07:38:10.979", {locale: "en-US"}).toUTC(), 38 | DateTime.fromISO("2019-07-23T07:38:28.263", {locale: "en-US"}).toUTC() 39 | ]); 40 | }); 41 | 42 | it("can return its classes", () => { 43 | expect(schedule.getAllClasses()).toEqual(bellScheduleClasses); 44 | }); 45 | 46 | it("can get the correct number of classes", () => { 47 | expect(schedule.numberOfClasses()).toEqual([classPeriod].length); 48 | }); 49 | 50 | //these cases already covered by tests for a class they extend 51 | // it("can return date last updated", () => { 52 | // expect(schedule.lastUpdated()).toEqual(DateTime.fromISO("2019-07-28T07:37:50.634", {zone: schoolTimezone}).toUTC()); 53 | // }); 54 | 55 | // it("can test if it has changed since a given date", () => { 56 | // expect(schedule.hasChangedSince(DateTime.fromISO("2019-07-28T07:07:50.634", {zone: schoolTimezone}).toUTC())).toBe(true); 57 | // expect(schedule.hasChangedSince(DateTime.fromISO("2019-07-28T08:07:50.634", {zone: schoolTimezone}).toUTC())).toBe(false); 58 | // }); 59 | 60 | it("can get a class period for a given time", () => { 61 | //before 62 | expect(schedule.getClassPeriodForTime(beforeClassDT, schoolTimezone)).toBeUndefined(); 63 | 64 | //exactly start 65 | expect(schedule.getClassPeriodForTime(startTimeDT, schoolTimezone)).toEqual(classPeriod); 66 | 67 | //middle 68 | expect(schedule.getClassPeriodForTime(duringClassDT, schoolTimezone)).toEqual(classPeriod); 69 | 70 | //between classes 71 | expect(schedule.getClassPeriodForTime(betweenClass, schoolTimezone)).toEqual(undefined); 72 | 73 | //exactly end 74 | expect(schedule.getClassPeriodForTime(endTimeDT, schoolTimezone)).toEqual(classPeriod); 75 | 76 | //after 77 | expect(schedule.getClassPeriodForTime(afterClassDT, schoolTimezone)).toBeUndefined(); 78 | }); 79 | 80 | it("can get a class period starting after a given time", () => { 81 | //before 1st class 82 | expect(schedule.getClassStartingAfter(beforeClassDT, schoolTimezone)).toEqual(classPeriod); 83 | 84 | //exactly start 1st class 85 | expect(schedule.getClassStartingAfter(startTimeDT, schoolTimezone)).toEqual(classPeriod2); 86 | 87 | //middle 1st class 88 | expect(schedule.getClassStartingAfter(duringClassDT, schoolTimezone)).toEqual(classPeriod2); 89 | 90 | //between classes 91 | expect(schedule.getClassStartingAfter(betweenClass, schoolTimezone)).toEqual(classPeriod2); 92 | 93 | //exactly end of 1st class 94 | expect(schedule.getClassStartingAfter(endTimeDT, schoolTimezone)).toEqual(classPeriod2); 95 | 96 | //after 1st class/before 2nd class 97 | expect(schedule.getClassStartingAfter(afterClassDT, schoolTimezone)).toEqual(classPeriod2); 98 | 99 | //exactly start 2nd class 100 | expect(schedule.getClassStartingAfter(startTime2DT, schoolTimezone)).toBeUndefined(); 101 | 102 | //middle 2nd class 103 | expect(schedule.getClassStartingAfter(duringClass2, schoolTimezone)).toBeUndefined(); 104 | 105 | //exactly end of 2nd class 106 | expect(schedule.getClassStartingAfter(endTime2DT, schoolTimezone)).toBeUndefined(); 107 | 108 | //after 2nd class 109 | expect(schedule.getClassStartingAfter(afterClass2, schoolTimezone)).toBeUndefined(); 110 | 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/@types/bellschedule.ts: -------------------------------------------------------------------------------- 1 | import ClassPeriod from "./classperiod"; 2 | import { DateTime } from "luxon"; 3 | import { TimeComparisons } from "../utils/enums"; 4 | import { getValueIfKeyInList, sortClassesByStartTime } from "../utils/helpers"; 5 | import UpdateTimestampedObject from "./updateTimestampedObject"; 6 | import Time from "./time"; 7 | 8 | export default class BellSchedule extends UpdateTimestampedObject { 9 | public static fromJson(json: any) { 10 | return new BellSchedule( 11 | getValueIfKeyInList(["id", "identifier"], json), 12 | getValueIfKeyInList(["name", "full_name", "fullName"], json), 13 | getValueIfKeyInList(["endpoint"], json), 14 | getValueIfKeyInList(["dates"], json).map((date: string) => DateTime.fromISO(date).toUTC()), 15 | getValueIfKeyInList(["classes", "meeting_times"], json).map( 16 | (meetingTime: any) => ClassPeriod.fromJson(meetingTime) 17 | ), 18 | DateTime.fromISO(getValueIfKeyInList(["last_modified", "lastModified"], json), { zone: 'utc' }) 19 | //display name 20 | ); 21 | } 22 | 23 | private id: string; 24 | private name: string; 25 | private endpoint: string; 26 | private displayName?: string; 27 | private dates: DateTime[]; 28 | private classes: ClassPeriod[]; 29 | private color?: string; 30 | 31 | constructor( 32 | id: string, 33 | name: string, 34 | endpoint: string, 35 | dates: DateTime[], 36 | classes: ClassPeriod[], 37 | lastUpdatedDate: DateTime, 38 | displayName?: string 39 | ) { 40 | super(lastUpdatedDate) 41 | this.id = id; 42 | this.name = name; 43 | this.endpoint = endpoint; 44 | this.displayName = displayName; 45 | this.dates = dates; 46 | this.classes = classes; 47 | 48 | } 49 | 50 | public getIdentifier() { 51 | return this.id; 52 | } 53 | 54 | public getName() { 55 | return this.name; 56 | } 57 | 58 | public setName(name: string) { 59 | this.name = name; 60 | } 61 | 62 | public setDisplayName(name: string) { 63 | this.displayName = name; 64 | } 65 | 66 | public getDisplayName() { 67 | if (this.displayName) { 68 | return this.displayName; 69 | } else { 70 | return this.name; 71 | } 72 | } 73 | 74 | public getEndpoint() { 75 | return this.endpoint; 76 | } 77 | 78 | public getDates() { 79 | return this.dates; 80 | } 81 | 82 | /**returns the actual date object from the schedule that has the same date as 83 | * the provided object. 84 | * used for checking if a schedule has a particular date as well as correcting 85 | * inaccuracies due to incorrect milliseconds .etc. 86 | * 87 | */ 88 | public getDate(date: DateTime) { 89 | for (const scheduleDate of this.getDates()) { 90 | if (scheduleDate.hasSame(date, "day")) { 91 | return scheduleDate; 92 | } 93 | } 94 | return; 95 | } 96 | 97 | public addDate(date: DateTime) { 98 | this.dates.push(date); 99 | } 100 | 101 | public removeDate(date: DateTime) { 102 | const actualDate = this.getDate(date); 103 | if (!actualDate){ 104 | return false; 105 | } 106 | const index = this.dates.indexOf(actualDate); 107 | return this.dates.splice(index, 1)[0]; 108 | } 109 | 110 | public getAllClasses() { 111 | return this.classes; 112 | } 113 | 114 | public getClassPeriodForTime(time: DateTime, schoolTimezone:string) { 115 | for (const classPeriod of sortClassesByStartTime(this.classes)) { 116 | if (classPeriod.stateForTime(Time.fromDateTime(time, schoolTimezone)) === TimeComparisons.IS_DURING_OR_EXACTLY) { 117 | return classPeriod; 118 | } 119 | } 120 | return; 121 | } 122 | 123 | public getClassStartingAfter(time: DateTime, schoolTimezone: string) { 124 | for (const classPeriod of sortClassesByStartTime(this.classes)) { 125 | if (classPeriod.stateForTime(Time.fromDateTime(time, schoolTimezone)) === TimeComparisons.IS_BEFORE) { 126 | return classPeriod; 127 | } 128 | } 129 | return; 130 | } 131 | 132 | //it would be better to call this last class index or drop the -1 133 | public numberOfClasses() { 134 | return this.classes.length - 1; 135 | } 136 | 137 | public addClass(classPeriod: ClassPeriod) { 138 | this.classes.push(classPeriod); 139 | } 140 | 141 | public removeClass(classPeriod: ClassPeriod) { 142 | const index = this.classes.indexOf(classPeriod); 143 | if (!index) { 144 | console.warn("attempt to remove nonexistent class period") 145 | return; 146 | } 147 | return this.classes.splice(index, 1)[0]; 148 | } 149 | 150 | public getColor(){ 151 | return this.color; 152 | } 153 | 154 | public setColor(color: string) { 155 | this.color = color; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/@types/classperiod.test.ts: -------------------------------------------------------------------------------- 1 | import ClassPeriod from "./classperiod"; 2 | import { TimeComparisons } from "../utils/enums"; 3 | import { classPeriod, className, startTime, endTime, classDuration, beforeClass, afterClass, duringClass, classPeriodJSON, currentDate, classPeriodJSONISO } from "../utils/testconstants"; 4 | 5 | export const classPeriodJSONObjectTime = { 6 | name: className, 7 | startTime, 8 | endTime, 9 | creationDate: currentDate 10 | }; 11 | 12 | describe("ClassPeriod", () => { 13 | 14 | it("can construct from json with standard string times", () => { 15 | //TODO: the start and end times should be plain HH:MM, not full datetimes 16 | 17 | let constructed = ClassPeriod.fromJson(classPeriodJSON) 18 | expect(constructed.getName()).toEqual(classPeriod.getName()); 19 | expect(constructed.getDuration()).toEqual(classPeriod.getDuration()); 20 | expect(constructed.getStartTime()).toEqual(classPeriod.getStartTime()); 21 | expect(constructed.getEndTime()).toEqual(classPeriod.getEndTime()); 22 | expect(constructed.getCreationDate()).toEqual(classPeriod.getCreationDate()); 23 | 24 | }); 25 | 26 | it("can construct from json with standard string times", () => { 27 | //TODO: the start and end times should be plain HH:MM, not full datetimes 28 | 29 | let constructed = ClassPeriod.fromJson(classPeriodJSONISO) 30 | expect(constructed.getName()).toEqual(classPeriod.getName()); 31 | expect(constructed.getDuration()).toEqual(classPeriod.getDuration()); 32 | expect(constructed.getStartTime()).toEqual(classPeriod.getStartTime()); 33 | expect(constructed.getEndTime()).toEqual(classPeriod.getEndTime()); 34 | expect(constructed.getCreationDate()).toEqual(classPeriod.getCreationDate()); 35 | 36 | }); 37 | 38 | it("can construct from json with object times", () => { 39 | // expect(ClassPeriod.fromJson(classPeriodJSONObjectTime)).toEqual(classPeriod); 40 | 41 | let constructed = ClassPeriod.fromJson(classPeriodJSONObjectTime) 42 | expect(constructed.getName()).toEqual(classPeriod.getName()); 43 | expect(constructed.getDuration()).toEqual(classPeriod.getDuration()); 44 | expect(constructed.getStartTime()).toEqual(classPeriod.getStartTime()); 45 | expect(constructed.getEndTime()).toEqual(classPeriod.getEndTime()); 46 | expect(constructed.getCreationDate()).toEqual(classPeriod.getCreationDate()); 47 | 48 | }); 49 | 50 | //gonna assume the constructor works 51 | 52 | it("can get its name", () => { 53 | expect(classPeriod.getName()).toBe(className); 54 | }); 55 | 56 | it("can get its start time", () => { 57 | expect(classPeriod.getStartTime()).toBe(startTime); 58 | }); 59 | 60 | it("can get its end time", () => { 61 | expect(classPeriod.getEndTime()).toBe(endTime); 62 | }); 63 | 64 | it("can get its duration", () => { 65 | expect(classPeriod.getDuration()).toEqual(classDuration); 66 | }); 67 | 68 | it("can check if a time falls in its range", () => { 69 | 70 | expect(classPeriod.stateForTime(beforeClass)).toBe(TimeComparisons.IS_BEFORE); 71 | 72 | expect(classPeriod.stateForTime(startTime)).toBe(TimeComparisons.IS_DURING_OR_EXACTLY); 73 | 74 | expect(classPeriod.stateForTime(duringClass)).toBe( 75 | TimeComparisons.IS_DURING_OR_EXACTLY 76 | ); 77 | 78 | expect(classPeriod.stateForTime(endTime)).toBe(TimeComparisons.IS_DURING_OR_EXACTLY); 79 | 80 | expect(classPeriod.stateForTime(afterClass)).toBe(TimeComparisons.IS_AFTER); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/@types/classperiod.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | import { checkTimeRange, getValueIfKeyInList } from "../utils/helpers"; 3 | import Time from "./time"; 4 | 5 | export default class ClassPeriod { 6 | public static fromJson(json: any) { 7 | const start = getValueIfKeyInList(["startTime", "start_time"], json); 8 | const end = getValueIfKeyInList(["endTime", "end_time"], json); 9 | return new ClassPeriod( 10 | getValueIfKeyInList(["name", "classPeriodName", "class_period_name"], json), 11 | Time.fromString(start), 12 | Time.fromString(end), 13 | DateTime.fromISO(getValueIfKeyInList(["creationDate", "creation_date"], json), {zone: 'utc'}) 14 | ); 15 | } 16 | private name: string; 17 | private startTime: Time; 18 | private endTime: Time; 19 | private creationDate: DateTime; 20 | 21 | constructor(name: string, startTime: Time, endTime: Time, creationDate: DateTime) { 22 | this.name = name; 23 | this.startTime = startTime; 24 | this.endTime = endTime; 25 | this.creationDate = creationDate; 26 | } 27 | 28 | public getName(): string { 29 | return this.name; 30 | } 31 | 32 | public setName(name: string) { 33 | this.name = name; 34 | } 35 | 36 | public getStartTime(): Time { 37 | return this.startTime; 38 | } 39 | 40 | public setStartTime(time: Time) { 41 | this.startTime = time; 42 | } 43 | 44 | public getEndTime(): Time { 45 | return this.endTime; 46 | } 47 | 48 | public setEndTime(time: Time) { 49 | this.endTime = time; 50 | } 51 | 52 | public getDuration(): Time { 53 | return this.startTime.getTimeDeltaTo(this.endTime); 54 | } 55 | 56 | public getCreationDate(): DateTime { 57 | return this.creationDate; 58 | } 59 | 60 | //remove me 61 | public stateForTime(time: Time) { 62 | return checkTimeRange(time, this.startTime, this.endTime); 63 | } 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/@types/redux-first-routing.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'redux-first-routing'{ 2 | 3 | // Type definitions for redux-first-routing 0.3 4 | // Project: https://github.com/mksarge/redux-first-routing 5 | // Definitions by: Tomek Łaziuk 6 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 7 | // TypeScript Version: 2.3 8 | 9 | import { createBrowserHistory } from 'history'; 10 | import { History, Pathname, Search, Hash, Path, LocationDescriptorObject } from 'history'; 11 | import { Action, Store, Reducer, Middleware } from 'redux'; 12 | 13 | export { createBrowserHistory }; 14 | 15 | export function startListener(history: History, store: Store): void; 16 | 17 | // constants 18 | export const PUSH: 'ROUTER/PUSH'; 19 | export const REPLACE: 'ROUTER/REPLACE'; 20 | export const GO: 'ROUTER/GO'; 21 | export const GO_BACK: 'ROUTER/GO_BACK'; 22 | export const GO_FORWARD: 'ROUTER/GO_FORWARD'; 23 | export const LOCATION_CHANGE: 'ROUTER/LOCATION_CHANGE'; 24 | 25 | // actions 26 | export interface pushAction extends Action { 27 | payload: T; 28 | } 29 | export function push(href: T): pushAction; 30 | 31 | export interface replaceAction extends Action { 32 | payload: T; 33 | } 34 | export function replace(href: T): replaceAction; 35 | 36 | export interface goAction extends Action { 37 | payload: T; 38 | } 39 | export function go(index: T): goAction; 40 | 41 | export type goBackAction = Action; 42 | export function goBack(): goBackAction; 43 | 44 | export type goForwardAction = Action; 45 | export function goForward(): goForwardAction; 46 | 47 | export interface locationChangeAction

extends Action { 48 | payload: { 49 | pathname: P; 50 | search: S; 51 | queries: any, 52 | hash: H, 53 | }; 54 | } 55 | export function locationChange

(_: { pathname: P, search: S, hash: H }): locationChangeAction; 56 | 57 | export interface State { 58 | pathname: Pathname; 59 | search: Search; 60 | queries: any; 61 | hash: Hash; 62 | } 63 | 64 | export const routerReducer: Reducer; 65 | 66 | export function routerMiddleware(history: History): Middleware; 67 | } -------------------------------------------------------------------------------- /src/@types/school.test.ts: -------------------------------------------------------------------------------- 1 | import School from "./school"; 2 | import { school, bellSchedule, schoolJSON, schoolId, schoolEndpoint, schoolName, schoolAcronym, passingPeriodName, schoolTimezone, currentDate, inClass, afterSchoolHours, schoolOwnerId, beforeSchoolHours, noSchool, betweenClass } from '../utils/testconstants'; 3 | 4 | const schoolNoSchedules = new School( 5 | schoolId, 6 | schoolOwnerId, 7 | schoolName, 8 | schoolAcronym, 9 | schoolEndpoint, 10 | schoolTimezone, 11 | [], 12 | passingPeriodName, 13 | currentDate, 14 | currentDate 15 | ); 16 | 17 | 18 | describe("School", () => { 19 | 20 | it("should get from JSON", () => { 21 | let constructed = School.fromJson(schoolJSON) 22 | //TODO: the start and end times should be plain HH:MM, not full datetimes 23 | expect(constructed.getName()).toEqual(school.getName()); 24 | expect(constructed.getAcronym()).toEqual(school.getAcronym()); 25 | expect(constructed.getSchedules()).toEqual(school.getSchedules()); 26 | expect(constructed.getTimezone()).toEqual(school.getTimezone()); 27 | expect(constructed.getPassingTimeName()).toEqual(school.getPassingTimeName()); 28 | expect(constructed.getIdentifier()).toEqual(school.getIdentifier()); 29 | expect(constructed.getEndpoint()).toEqual(school.getEndpoint()); 30 | expect(constructed.getAcronym()).toEqual(school.getAcronym()); 31 | 32 | }); 33 | 34 | //assuming constructor works, although maybe it could be tested against the fromJSON method? 35 | 36 | it("can return identifier", () => { 37 | expect(school.getIdentifier()).toBe(schoolId); 38 | }); 39 | 40 | it("can return API endpoint", () => { 41 | expect(school.getEndpoint()).toBe(schoolEndpoint); 42 | }); 43 | 44 | it("can return schedules", () => { 45 | expect(school.getSchedules()).toEqual([bellSchedule]); 46 | }); 47 | 48 | it("can return name", () => { 49 | expect(school.getName()).toBe(schoolName); 50 | }); 51 | 52 | it("can return acronym", () => { 53 | expect(school.getAcronym()).toBe(schoolAcronym); 54 | }); 55 | 56 | it("can return passing time name", () => { 57 | expect(school.getPassingTimeName()).toBe(passingPeriodName); 58 | }); 59 | 60 | it("can return timezone", () => { 61 | expect(school.getTimezone()).toBe(schoolTimezone); 62 | }); 63 | 64 | it("can return creation date", () => { 65 | expect(school.getCreationDate()).toEqual(currentDate); 66 | }); 67 | 68 | //these cases already covered by tests for a class they extend 69 | // it("can return date last updated", () => { 70 | // expect(school.lastUpdated()).toEqual(currentDate); 71 | // }); 72 | 73 | // it("can Test if it has changed since a given date", () => { 74 | // //school was last updated on currentDate 75 | // expect(school.hasChangedSince(currentDate.minus({ hours: 1 }))).toBe(true); 76 | // expect(school.hasChangedSince(currentDate.plus({ hours: 1 }))).toBe(false); 77 | 78 | // expect(school.hasChangedSince(DateTime.fromISO("2019-07-28T07:07:50.634", { zone: schoolTimezone }))).toBe(true); 79 | // // "2019-07-28T07:37:50.634" 80 | // expect(school.hasChangedSince(DateTime.fromISO("2019-07-28T08:07:50.635", { zone: schoolTimezone }))).toBe(false); 81 | // }); 82 | 83 | it("can get schedule for date", () => { 84 | expect(school.getScheduleForDate(currentDate)).toEqual( 85 | bellSchedule 86 | ); 87 | 88 | expect(schoolNoSchedules.getScheduleForDate(currentDate)).toBe(null); 89 | }); 90 | 91 | it("can check if it has schedules", () => { 92 | expect(school.hasSchedules()).toBe(true); 93 | expect(schoolNoSchedules.hasSchedules()).toBe(false); 94 | }); 95 | 96 | it("can check if school is in session", () => { 97 | expect(school.isInSession(inClass)).toBe(true); 98 | expect(school.isInSession(beforeSchoolHours)).toBe(false); 99 | expect(school.isInSession(noSchool)).toBe(false); 100 | expect(school.isInSession(afterSchoolHours)).toBe(false); 101 | expect(school.isInSession(betweenClass)).toBe(true); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/@types/school.ts: -------------------------------------------------------------------------------- 1 | import { 2 | checkTimeRange, 3 | getValueIfKeyInList, 4 | sortClassesByStartTime 5 | } from "../utils/helpers"; 6 | import { DateTime } from "luxon"; 7 | import { TimeComparisons } from "../utils/enums"; 8 | import BellSchedule from "./bellschedule"; 9 | import find from 'lodash.find' 10 | import UpdateTimestampedObject from "./updateTimestampedObject"; 11 | import Time from "./time"; 12 | 13 | export default class School extends UpdateTimestampedObject { 14 | public static fromJson(json: any) { 15 | const schedules = getValueIfKeyInList(["schedules"], json); 16 | return new School( 17 | getValueIfKeyInList(["id", "identifier"], json), 18 | getValueIfKeyInList(["ownerId", "owner_id"], json), 19 | getValueIfKeyInList(["name", "fullName", "full_name"], json), 20 | getValueIfKeyInList(["acronym"], json), 21 | getValueIfKeyInList(["endpoint"], json), 22 | getValueIfKeyInList(["timezone"], json), 23 | schedules 24 | ? schedules.map((schedule: any) => BellSchedule.fromJson(schedule)) 25 | : undefined, 26 | getValueIfKeyInList(["alternate_freeperiod_name", "passingPeriodName"], json), 27 | DateTime.fromISO(getValueIfKeyInList(["creation_date", "creationDate"], json), { zone: 'utc' }), 28 | DateTime.fromISO(getValueIfKeyInList(["last_modified", "lastModified"], json), { zone: 'utc' }) 29 | ); 30 | } 31 | 32 | private id: string; 33 | private ownerId: string; 34 | private endpoint?: string; 35 | private fullName: string; 36 | private acronym: string; 37 | private timeZone: string; 38 | private schedules?: BellSchedule[]; 39 | private passingPeriodName?: string; 40 | private creationDate?: DateTime; 41 | 42 | constructor( 43 | id: string, 44 | ownerId: string, 45 | fullName: string, 46 | acronym: string, 47 | endpoint: string, 48 | timeZone: string, 49 | schedules?: BellSchedule[], 50 | passingPeriodName?: string, 51 | creationDate?: DateTime, 52 | lastUpdatedDate?: DateTime 53 | ) { 54 | super(lastUpdatedDate) 55 | this.id = id; 56 | this.ownerId = ownerId; 57 | this.endpoint = endpoint; 58 | this.fullName = fullName; 59 | this.acronym = acronym; 60 | this.timeZone = timeZone; 61 | this.schedules = schedules; 62 | this.passingPeriodName = passingPeriodName; 63 | this.creationDate = creationDate; 64 | 65 | } 66 | 67 | public getIdentifier(): string { 68 | return this.id; 69 | } 70 | 71 | public getOwnerIdentifier(): string { 72 | return this.ownerId; 73 | } 74 | 75 | public getEndpoint() { 76 | return this.endpoint; 77 | } 78 | 79 | public getSchedules() { 80 | return this.schedules; 81 | } 82 | 83 | public getSchedule(id: string) { 84 | if (!this.schedules){ 85 | return 86 | } else { 87 | return find(this.schedules, schedule => { return schedule.getIdentifier() === id; }); 88 | } 89 | } 90 | 91 | public getName() { 92 | return this.fullName; 93 | } 94 | 95 | public getAcronym() { 96 | return this.acronym; 97 | } 98 | 99 | public getPassingTimeName() { 100 | return this.passingPeriodName; 101 | } 102 | 103 | public getTimezone() { 104 | return this.timeZone; 105 | } 106 | 107 | public getCreationDate() { 108 | return this.creationDate; 109 | } 110 | 111 | //can also be used as isNoSchoolDay() by checking for undefined 112 | public getScheduleForDate(date: DateTime) { 113 | if (this.schedules) { 114 | for (const schedule of this.schedules) { 115 | if (schedule.getDate(date)) { 116 | return schedule; 117 | } 118 | } 119 | return null; //no schedule today 120 | } 121 | return; // no schedules defined 122 | } 123 | 124 | //remove 125 | public hasSchedules() { 126 | return this.schedules !== undefined && this.schedules.length > 0; 127 | } 128 | 129 | //change input to a time 130 | //seems like te current schedule depends on this 131 | /** 132 | * Checks whether school is currently "in session", meaning that school is currently happening for the day (aka a time is between the start of the first class and the end of the last class) 133 | * @param date the time to check 134 | * @returns true if school is in session, false otherwise 135 | */ 136 | public isInSession(date: DateTime): boolean { 137 | const currentSchedule = this.getScheduleForDate(date); 138 | if (!currentSchedule) { 139 | return false; 140 | } 141 | 142 | const sortedClasses = sortClassesByStartTime(currentSchedule.getAllClasses()) 143 | const firstClass = sortedClasses[0] 144 | const lastClass = sortedClasses[currentSchedule.numberOfClasses()] 145 | return ( 146 | checkTimeRange( 147 | Time.fromDateTime(date, this.timeZone), 148 | firstClass.getStartTime(), 149 | lastClass.getEndTime() 150 | ) == TimeComparisons.IS_DURING_OR_EXACTLY 151 | ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/@types/time.test.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | import Time from "./time"; 3 | import {school} from "../utils/testconstants"; 4 | 5 | const thisTime: Time = Time.fromTime(9, 31, 41); 6 | const thisTimeUTC: Time = Time.fromISO("2022-06-16T09:31:41Z"); 7 | const preTime: Time = Time.fromTime(5, 18, 43); 8 | const postTime: Time = Time.fromTime(13, 44, 39); 9 | 10 | describe("Time", () => { 11 | it("can instantiate from milliseconds", () => { 12 | expect(Time.fromMilliseconds(34301000)).toEqual(thisTime); 13 | }); 14 | 15 | it("can instantiate from DateTime", () => { 16 | let testTime1 = Time.fromDateTime(DateTime.fromISO("2022-06-16T09:31:41", { zone: school.getTimezone() }), school.getTimezone()); 17 | let testTime2 = Time.fromDateTime(DateTime.fromISO("2022-07-16T09:31:41", { zone: school.getTimezone()}), school.getTimezone()) 18 | 19 | expect(testTime1.hours).toEqual(thisTime.hours); 20 | expect(testTime1.minutes).toEqual(thisTime.minutes); 21 | expect(testTime1.seconds).toEqual(thisTime.seconds); 22 | 23 | //ensure dates are correctly stripped out so two different timestamps 24 | //can represent the same date 25 | expect(testTime1).toEqual(testTime2); 26 | 27 | }); 28 | 29 | it("should get from string", () => { 30 | expect(Time.fromString("9:31:41")).toEqual(thisTime); 31 | }); 32 | 33 | it("should get from string with leading zeroes", () => { 34 | expect(Time.fromString("08:04:09")).toEqual(Time.fromTime(8, 4, 9)); 35 | }); 36 | 37 | it("should get from string without seconds", () => { 38 | expect(Time.fromString("08:04")).toEqual(Time.fromTime(8, 4, 0)); 39 | }); 40 | 41 | it("should correct for values that are too large", () => { 42 | expect(Time.fromTime(45, 130, 118)).toEqual(Time.fromTime(21, 10, 58)); 43 | }); 44 | 45 | it("should correct for negative values", () => { 46 | expect(Time.fromTime(-9, -31, -41)).toEqual(thisTime); 47 | }); 48 | 49 | it("should correct for values that are too large and negative", () => { 50 | expect(Time.fromTime(-45, -130, -118)).toEqual(Time.fromTime(21, 10, 58)); 51 | }); 52 | 53 | it("should return hours", () => { 54 | expect(thisTime.hours).toBe(9); 55 | // expect(thisTime.getHours()).toBe(9); 56 | }); 57 | 58 | it("should return minutes", () => { 59 | expect(thisTime.minutes).toBe(31); 60 | // expect(thisTime.getMinutes()).toBe(31); 61 | }); 62 | 63 | it("should return seconds", () => { 64 | expect(thisTime.seconds).toBe(41); 65 | // expect(thisTime.getSeconds()).toBe(41); 66 | }); 67 | 68 | it("can detect if a time is before it", () => { 69 | expect(thisTime.isAfter(preTime)).toBe(true); 70 | expect(thisTime.isAfter(postTime)).toBe(false); 71 | }); 72 | 73 | it("can detect if a time is after it", () => { 74 | expect(thisTime.isBefore(preTime)).toBe(false); 75 | expect(thisTime.isBefore(postTime)).toBe(true); 76 | }); 77 | 78 | it("should get the number of milliseconds to a future time", () => { 79 | expect(thisTime.getMillisecondsTo(postTime)).toBe(15178000); 80 | }); 81 | 82 | it("should get the number of milliseconds to a past time", () => { 83 | expect(thisTime.getMillisecondsTo(preTime)).toBe(-15178000); 84 | }); 85 | 86 | it("should get the number of milliseconds to the same time", () => { 87 | expect(thisTime.getMillisecondsTo(thisTime)).toBe(0); 88 | expect(preTime.getMillisecondsTo(preTime)).toBe(0); 89 | expect(postTime.getMillisecondsTo(postTime)).toBe(0); 90 | }); 91 | 92 | it("returns the correct time delta", () => { 93 | expect(thisTime.getTimeDeltaTo(preTime)).toEqual(Time.fromMilliseconds(15178000)); 94 | expect(thisTime.getTimeDeltaTo(postTime)).toEqual( 95 | Time.fromMilliseconds(15178000) 96 | ); 97 | }); 98 | 99 | it("can return times as strings", () => { 100 | expect(thisTime.toString()).toBe("09:31:41"); 101 | expect(preTime.toString()).toBe("05:18:43"); 102 | expect(postTime.toString()).toBe("13:44:39"); 103 | }); 104 | 105 | it("can get formatted strings in the morning", () => { 106 | expect(thisTime.getFormattedString(true, true)).toBe("09:31"); 107 | expect(thisTime.getFormattedString(true, false)).toBe("09:31 AM"); 108 | expect(thisTime.getFormattedString(false, true)).toBe("09:31:41"); 109 | expect(thisTime.getFormattedString(false, false)).toBe("09:31:41 AM"); 110 | }); 111 | 112 | it("can get formatted strings in the afternoon", () => { 113 | expect(postTime.getFormattedString(true, true)).toBe("13:44"); 114 | expect(postTime.getFormattedString(true, false)).toBe("01:44 PM"); 115 | expect(postTime.getFormattedString(false, true)).toBe("13:44:39"); 116 | expect(postTime.getFormattedString(false, false)).toBe("01:44:39 PM"); 117 | }); 118 | 119 | it("serializes to a string", () => { 120 | expect(thisTime.toJSON()).toBe("09:31:41"); 121 | }); 122 | 123 | it("has a json representation matching that of the string output", () => { 124 | expect(thisTime.toJSON()).toEqual(thisTime.getFormattedString(false, true)); 125 | expect(thisTimeUTC.toJSON()).toEqual(thisTimeUTC.getFormattedString(false, true)); 126 | 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /src/@types/time.ts: -------------------------------------------------------------------------------- 1 | import { DateTime, Duration } from "luxon"; 2 | 3 | /** 4 | * A representation of a Time without an associated date. 5 | * This is used to simplify the JSON data since the same time ranges for a class 6 | * often apply to multiple days. 7 | * 8 | * The "standard" way to represent it in serialized/JSON form is as a series of 9 | * Two or three, colon-separated, two-digit numbers representing a 24-hour time. 10 | * examples: 09:35:00, 13:30:00, 09:35, 13:30 11 | * 12 | * Times in this standard format should be considered to be in the timezone of 13 | * the school that they are part of. 14 | * 15 | * A Time object should always represent this standard form of the 16 | * 17 | * @export 18 | * @class Time 19 | */ 20 | export default class Time { 21 | 22 | private duration: Duration; 23 | 24 | constructor(duration: Duration) { 25 | this.duration = duration 26 | } 27 | 28 | public get hours() { 29 | return this.duration.hours 30 | } 31 | 32 | public get minutes() { 33 | return this.duration.minutes 34 | } 35 | 36 | public get seconds() { 37 | return this.duration.seconds 38 | } 39 | 40 | /** 41 | * Create a time instance from the number of milliseconds since the beginning of the day. 42 | * 43 | * @static 44 | * @param {number} milliseconds the number of milliseconds since the beginning of the day 45 | * @returns {Time} 46 | * @memberof Time 47 | */ 48 | public static fromMilliseconds(milliseconds: number): Time { 49 | return new Time(Duration.fromMillis(milliseconds).shiftTo("hours", "minutes", "seconds")); 50 | } 51 | 52 | //Deprecated 53 | // public static fromJSDate(date: Date, toLocalTime=false) { 54 | // if (toLocalTime) { 55 | // return new Time(date.getHours(), date.getMinutes(), date.getSeconds(), 'local'); 56 | // } else { 57 | // return new Time(date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), 'utc'); 58 | // } 59 | // } 60 | 61 | public static fromISO(time: string) { 62 | return new Time(Duration.fromISO(time)) 63 | } 64 | 65 | /** 66 | * Create a time instance from the JSON-serialized data produced by `toJSON()` 67 | * @param time a time value represented as a string in standard format to deserialize to a Time Object 68 | * @returns a n instance of Time representing the same time 69 | */ 70 | public static fromJson(time: string) { 71 | const parts = time.split(":"); 72 | if (parts.length < 2 || parts.length > 3) { 73 | //TODO: error here 74 | } 75 | return Time.fromTime( 76 | parseInt(parts[0], 10), 77 | parseInt(parts[1], 10), 78 | parts.length === 3 ? parseInt(parts[2], 10) : undefined 79 | ); 80 | } 81 | 82 | public static fromString(time: string) { 83 | // const smalltime = DateTime.fromFormat(time, "H:mm") 84 | // const bigtime = DateTime.fromISO(time) 85 | // if (smalltime.isValid) { 86 | // return Time.fromDateTime(smalltime)// .toUTC() 87 | // } else if (bigtime.isValid) { 88 | // return Time.fromDateTime(bigtime)// .toUTC() 89 | // } 90 | 91 | const parts = time.split(":"); 92 | if (parts.length < 2 || parts.length > 3) { 93 | //TODO: error here 94 | } 95 | return Time.fromTime( 96 | parseInt(parts[0], 10), 97 | parseInt(parts[1], 10), 98 | parts.length === 3 ? parseInt(parts[2], 10) : undefined 99 | ); 100 | } 101 | 102 | /** 103 | * Create a Time from the time portion of the given DateTime 104 | * @param time the DateTime 105 | * @returns a Time object 106 | */ 107 | public static fromDateTime(time: DateTime, schoolTimezone: string) { 108 | time = time.setZone(schoolTimezone) 109 | 110 | return new Time(time.diff(time.startOf('day')).shiftTo("hours", "minutes", "seconds")) 111 | } 112 | 113 | public static fromTime(hours: number, minutes: number, seconds?: number): Time { 114 | // super() 115 | // why do we need timezone, 116 | //is storing the time as a DateTime internally just super overkill? 117 | // probably to allow conversion later? 118 | let timeObj = { 119 | hour: Math.abs((hours || 0) % 24), 120 | minute: Math.abs((minutes || 0) % 60), 121 | second: Math.abs((seconds || 0) % 60) 122 | } 123 | return new Time(Duration.fromObject(timeObj).shiftTo("hours", "minutes", "seconds")) 124 | } 125 | 126 | public getMillisecondsTo(otherTime: Time): number { 127 | return otherTime.duration.minus(this.duration).toMillis() 128 | } 129 | 130 | public isBefore(time:Time): boolean { 131 | return this.duration < time.duration 132 | } 133 | 134 | public isAfter(time: Time): boolean { 135 | return this.duration > time.duration 136 | } 137 | 138 | public isEqualTo(time: Time): boolean { 139 | return this.duration == time.duration 140 | } 141 | 142 | public getTimeDeltaTo(otherTime: Time): Time { 143 | return Time.fromMilliseconds(Math.abs(this.getMillisecondsTo(otherTime)??0)); 144 | } 145 | 146 | public toString(excludeSeconds = false, use24HourTime = true) { 147 | const seconds = excludeSeconds ? "" : ":ss" 148 | const format = "hh:mm" + seconds 149 | let meridiem = "" 150 | let duration = this.duration 151 | 152 | if (!use24HourTime && duration.hours > 12) { 153 | duration = duration.minus(Duration.fromObject({ hours: 12 })) 154 | meridiem = "PM" 155 | } else { 156 | meridiem = "AM" 157 | } 158 | 159 | return duration.toFormat(format) + ((use24HourTime)? "" : " " + meridiem); 160 | } 161 | 162 | /** 163 | * Convert this datetime into a formatted string. 164 | * Leaving values at the defaults should generate a "standard" string with 165 | * three parts 166 | * @param excludeSeconds whether to exclude the seconds values, default false. 167 | * @param use24HourTime whether to use 24 hour time or 12 hour time. Default true. 168 | * @returns a string representing the current time 169 | */ 170 | public getFormattedString(excludeSeconds = false, use24HourTime = false) { 171 | return this.toString(excludeSeconds, use24HourTime) 172 | } 173 | 174 | /** 175 | * Returns the standard HH:mm:ss representation of a time object as a string for serializing. 176 | * 177 | * this overrides the automatic serialization of Time Objects and makes them return a string and not a plain object (which is more annoying to parse back in and would require an extra factory method) 178 | */ 179 | public toJSON(): string { 180 | return this.duration.toFormat("hh:mm:ss"); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/@types/updateTimestampedObject.test.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | import { schoolTimezone } from "../utils/testconstants"; 3 | import UpdateTimestampedObject from "./updateTimestampedObject"; 4 | 5 | let lastUpdated = DateTime.fromISO("2019-07-28T07:37:50.634Z") 6 | let sut = new UpdateTimestampedObject(lastUpdated) 7 | 8 | 9 | describe("UpdateTimestampedObect", () => { 10 | 11 | it("can return date last updated", () => { 12 | expect(sut.lastUpdated()).toEqual(lastUpdated); 13 | }); 14 | 15 | it("can Test if it has changed since a given date", () => { 16 | //school was last updated on currentDate 17 | expect(sut.hasChangedSince(lastUpdated.minus({ hours: 1 }))).toBe(true); 18 | expect(sut.hasChangedSince(lastUpdated.plus({ hours: 1 }))).toBe(false); 19 | 20 | expect(sut.hasChangedSince(DateTime.fromISO("2019-07-28T00:07:50.634", { zone: schoolTimezone }))).toBe(true); 21 | expect(sut.hasChangedSince(DateTime.fromISO("2019-07-28T08:07:50.635", { zone: schoolTimezone }))).toBe(false); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/@types/updateTimestampedObject.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | 3 | 4 | /** 5 | * An object that knows when it was last updated 6 | * 7 | * @export 8 | * @class UpdateTimestampedObject 9 | */ 10 | export default class UpdateTimestampedObject { 11 | protected lastUpdatedDate?: DateTime; 12 | 13 | constructor(lastUpdatedDate?:DateTime){ 14 | this.lastUpdatedDate = lastUpdatedDate; 15 | } 16 | 17 | public lastUpdated() { 18 | return this.lastUpdatedDate; 19 | } 20 | 21 | public hasChangedSince(date: DateTime) { 22 | // date = date.toUTC(); 23 | if (this.lastUpdatedDate !== undefined) { 24 | return date.toMillis() < this.lastUpdatedDate.toMillis(); 25 | } else { 26 | return undefined; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/components/Block/Block.css: -------------------------------------------------------------------------------- 1 | .infoBlock { 2 | margin: 10px 0; 3 | } 4 | 5 | .infoBlock * { 6 | margin: 1px 0; 7 | padding: 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Block/Block.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import Block from "./Block"; 4 | 5 | 6 | describe("Block Component", () => { 7 | it("renders correctly", () => { 8 | const component = renderer.create(); 9 | const tree = component.toJSON(); 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/Block/Block.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, CSSProperties } from "react"; 2 | import "./Block.css"; 3 | 4 | interface ITextProps { 5 | className?: string; 6 | style?: CSSProperties; 7 | } 8 | 9 | /** 10 | * A block represents a single piece of information in the main classclock app interface, such as the time left in this class, what the next class is, what the current class is .etc 11 | */ 12 | export default class Block extends Component { 13 | render() { 14 | return ( 15 |

21 | {this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Block/__snapshots__/Block.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Block Component renders correctly 1`] = ` 4 |
7 | `; 8 | -------------------------------------------------------------------------------- /src/components/BlockLink.tsx: -------------------------------------------------------------------------------- 1 | import Link, { ILinkProps } from "./Link" 2 | 3 | 4 | const BlockLink = (props: ILinkProps) => { 5 | 6 | return 7 | {props.children} 8 | 9 | } 10 | 11 | export default BlockLink; -------------------------------------------------------------------------------- /src/components/Icon.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import Icon from "./Icon"; 4 | 5 | describe("Icon", () => { 6 | it("renders correctly", () => { 7 | const component = renderer.create( 8 | 9 | ); 10 | const tree = component.toJSON(); 11 | expect(tree).toMatchSnapshot(); 12 | 13 | }); 14 | }); -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | interface IIconProps { 4 | icon: string; 5 | regularStyle?:boolean 6 | } 7 | 8 | const Icon = (props: IIconProps) => { 9 | const style = props.regularStyle? "far ": "fas "; 10 | return ; 11 | } 12 | 13 | export default Icon; -------------------------------------------------------------------------------- /src/components/Link.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import renderer from "react-test-renderer"; 3 | import Link from "./Link"; 4 | 5 | 6 | describe("Link Component", () => { 7 | it("renders correctly with a static link", () => { 8 | const component = renderer.create( 9 | 10 | ); 11 | const tree = component.toJSON(); 12 | expect(tree).toMatchSnapshot(); 13 | 14 | // // manually trigger the callback 15 | // tree.props.onMouseEnter(); 16 | // re-rendering 17 | // tree = component.toJSON(); 18 | // expect(tree).toMatchSnapshot(); 19 | }); 20 | 21 | it("renders correctly with a function", () => { 22 | let pass = false; 23 | const onClick = () => {pass = true} 24 | const component = renderer.create(); 25 | const tree = component.toJSON(); 26 | expect(tree).toMatchSnapshot(); 27 | 28 | if (tree != null) { 29 | // manually trigger the callback 30 | component.root.findByType('a').props.onClick(); 31 | } 32 | 33 | 34 | expect(pass).toBeTruthy(); 35 | 36 | }); 37 | 38 | }); 39 | -------------------------------------------------------------------------------- /src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, ReactNode } from "react"; 2 | 3 | export interface ILinkProps { 4 | destination: any; 5 | className?: string; 6 | style?: CSSProperties; 7 | title?: string; 8 | id?: string; 9 | children?: ReactNode | ReactNode[]; 10 | } 11 | 12 | const Link = (props: ILinkProps) => { 13 | 14 | return ( 15 | 31 | {props.children} 32 | 33 | ); 34 | } 35 | 36 | export default Link; 37 | -------------------------------------------------------------------------------- /src/components/List/List.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface IListProps { 4 | items: JSX.Element[]; 5 | } 6 | 7 | const List = (props: IListProps) => { 8 | return
{props.items}
; 9 | }; 10 | 11 | export default List; 12 | -------------------------------------------------------------------------------- /src/components/ScheduleEntry/ScheduleEntry.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface IScheduleEntryProps { 4 | name: string; 5 | startTime: string; 6 | endTime: string; 7 | } 8 | 9 | const ScheduleEntry = (props: IScheduleEntryProps) => { 10 | return ( 11 |
12 | {props.name}: {props.startTime} - {props.endTime} 13 |
14 | ); 15 | }; 16 | 17 | export default ScheduleEntry; 18 | -------------------------------------------------------------------------------- /src/components/SelectionList/SelectionList.css: -------------------------------------------------------------------------------- 1 | ul.selectionList { 2 | padding: 0; /* have no clue whats causing the random left padding of 20px on