├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── work-commute.iml ├── LICENSE ├── README.md ├── now.json ├── package.json ├── readme ├── illustration.png ├── shortcuts_1.jpeg ├── webapp_1.png └── webapp_2.png ├── server ├── api │ ├── log-event.ts │ ├── log-home-arrive.ts │ ├── log-home-leave.ts │ ├── log-work-arrive.ts │ └── log-work-leave.ts ├── constants.ts ├── graphql │ ├── index.ts │ ├── interface.ts │ ├── resolvers │ │ ├── day │ │ │ ├── index.ts │ │ │ ├── total-evening-commute-time.ts │ │ │ ├── total-morning-commute-time.ts │ │ │ └── total-time-at-office.ts │ │ ├── index.ts │ │ ├── period │ │ │ ├── average-time-at-office.ts │ │ │ ├── average-time-commuting.ts │ │ │ ├── index.ts │ │ │ ├── timetable-chart.ts │ │ │ └── total-time-at-office.ts │ │ └── query │ │ │ ├── day.ts │ │ │ ├── first-record-info.ts │ │ │ ├── index.ts │ │ │ └── period.ts │ └── type-defs.ts ├── interfaces.ts ├── lib │ └── db.ts ├── package.json ├── tsconfig.json ├── utils │ ├── get-time-from-minutes.ts │ └── log-time.ts └── yarn.lock ├── webapp ├── .gitignore ├── .npmrc ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.tsx │ ├── components │ │ ├── Averages │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ ├── query.ts │ │ │ └── styled.tsx │ │ ├── Card │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ ├── DayTimetable │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── query.ts │ │ ├── DayTotal │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ ├── query.ts │ │ │ └── styled.tsx │ │ ├── IconLabel │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ ├── ListItemPicker │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ ├── LoadingSpinner │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ ├── MonthPicker │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ ├── query.ts │ │ │ ├── styled.tsx │ │ │ └── use-calendar-data.ts │ │ ├── Navigation │ │ │ ├── index.tsx │ │ │ └── styled.tsx │ │ ├── Period │ │ │ ├── index.tsx │ │ │ └── styled.tsx │ │ ├── PeriodBarChart │ │ │ ├── __mock__ │ │ │ │ └── data.ts │ │ │ ├── animations.ts │ │ │ ├── carousel-chart.tsx │ │ │ ├── chart-bar.tsx │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ ├── query.ts │ │ │ ├── status-information.tsx │ │ │ ├── styled.tsx │ │ │ └── utils.ts │ │ ├── QueryErrorIcon │ │ │ ├── index.tsx │ │ │ └── styled.tsx │ │ ├── Section │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ ├── Slider │ │ │ ├── index.ts │ │ │ └── slick.css │ │ ├── TimeDisplay │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ ├── TimetableDisplay │ │ │ ├── index.tsx │ │ │ ├── interface.ts │ │ │ └── styled.tsx │ │ └── Today │ │ │ ├── index.tsx │ │ │ └── styled.tsx │ ├── constants.ts │ ├── global-styles.tsx │ ├── index.tsx │ ├── interfaces.ts │ └── react-app-env.d.ts ├── tsconfig.json └── yarn.lock └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | 9 | # Firebase cache 10 | .firebase/ 11 | 12 | # Firebase config 13 | 14 | # Uncomment this if you'd like others to create their own Firebase project. 15 | # For a team working on the same Firebase project(s), it is recommended to leave 16 | # it commented so all members can deploy to the same project(s) in .firebaserc. 17 | # .firebaserc 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (http://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | 49 | # Optional npm cache directory 50 | .npm 51 | 52 | # Optional eslint cache 53 | .eslintcache 54 | 55 | # Optional REPL history 56 | .node_repl_history 57 | 58 | # Output of 'npm pack' 59 | *.tgz 60 | 61 | # Yarn Integrity file 62 | .yarn-integrity 63 | 64 | # dotenv environment variables file 65 | .env 66 | .env.build 67 | 68 | #mongodb 69 | dump/ 70 | prod_dump.json 71 | 72 | # macos 73 | .DS_Store 74 | 75 | # CRA 76 | build/ 77 | 78 | # Local mongodb script 79 | db_script.js 80 | .now 81 | .idea/watcherTasks.xml 82 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml 3 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/work-commute.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rafael Violato 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Work commute web app 2 | 3 | 4 | 5 | Webapp for tracking how much time I spent commuting and working every day. 6 | 7 | ### [Check it out!](https://work-commute-wine-psi.now.sh/period) 8 | 9 | # Motivation 10 | 11 | I am a bit obsessed with my own time and how it is being spent, and just like most of the modern humans, we spend a good amount of our time working and commuting to work, so I thought I could write a web app to keep track of how much time I spend doing that. 12 | 13 | Besides wanting to give myself a more full-stack challenge, I also wanted to learn some technical stuff that I did not have the chance of using before in an application, which are: 14 | 15 | - Typescript 16 | - Zeit Now 17 | - GraphQL 18 | - MongoDB 19 | - React-hooks 20 | - CSS-in-JS 21 | - CSS Grid 22 | 23 | And what is a better way of learning something other than getting your hands dirty with it? So there you go. 24 | 25 | # How it works 26 | 27 | The needs of a time tracker are very straight-forward, you have to log time and visualize it in a way you can see how much time you're spending. So I thought to have a more granular way of measuring time, I could just use a few events to mark when I started or stopped doing something so I could show more information. 28 | As for visualization, now only I wanted a quick way to check how my day has been going, but I also have a way of checking previous days, so I could have an idea of how much time I was spending on those past periods. 29 | 30 | ## Logging 31 | 32 | This part was the one that had to put most thought on, I really wanted to simplify the way I can log those time events, otherwise, this task would become tedious and there should be a way for this web app to remind me or give me daily hints, otherwise, I would end up forget doing it in a daily basis. 33 | 34 | At first, I thought of doing some scheduled browser push-notifications, but that would take me much more development time, so as I mentioned earlier I am very obsessed with time and I also really like to automate some aspects of my life, so I really like the iOS Shortcuts app for making some repetitive tasks more trivial, and while investigating its features, I noticed that you could do network requests on those shortcuts, so this was the perfect way of logging those time events. 35 | 36 | On late 2019, there was an update on the Shortcuts app which you could now make automation to run shortcuts, and one of the automation features involve geolocation, so you can trigger any shortcut depending on where you are arriving and leaving, so that was the perfect use case for the reminder need. 37 | 38 | 39 | 40 | ### Time Event API 41 | 42 | For those shortcuts to work, I created a REST API that is simple enough for those shortcuts to call it, and I decided to use Zeit Now's serverless functions for creating small and simple functions that would become endpoints. 43 | 44 | ## Displaying the data 45 | 46 | This was the most straight-forward part to think of, It had to be just that good old dashboard full of UI cards enhanced with data and a chart to display the data distributed over a period of time. 47 | 48 | ### Today section 49 | 50 | 51 | 52 | This is for the most day-to-day use. I always want to check how much time I took to get to work or how much time I have been working already for that day. 53 | 54 | ### Period section 55 | 56 | 57 | 58 | Here is where I would be able to visualize how a period has been. For now, it is only possible to select a month period, so the chart looks nicer and is not cluttered and that has been proving to be enough so far. 59 | 60 | # Challenges 61 | 62 | There were a couple of challenges that I put to myself to learn some things that I wanted. 63 | 64 | ### Hooks 65 | 66 | Try to do all React related code using functional components, and to achieve that hooks were an amazing tool and were nice to start getting used to how they work and the caveats of only using hooks for common patterns that I was used to solving with classes' lifecycle callbacks. So far I have not seen any case that I would have been done better or easier with classes. 67 | 68 | ### GraphQL 69 | 70 | Before this project I did only hello-world tiny projects to play around with it but never had the pleasure of working with it, so I wanted to challenge myself to not only use GQL but also to try to craft the resolvers and the schema in a way which the UI becomes very "dumb" and only worries about UI-specific state and logic, so all the business logic and data normalization processes are centered on the GQL side. And oh boy, how that was indeed proven to be a nice way to do things. 71 | 72 | I haven't felt the need to install redux (or try to use mobx which I still want to create a project to use it) at any time, and it made things so much simpler. Coding the UI was fluid and natural process of only thinking about how the UI is going to behave and nothing else. 73 | 74 | Also, I wanted to try-out something I always thought that GQL could shine, which is making per-component queries that gather only the data that is necessary for that component and nothing else, and apollo really helped making this process very pleasant, especially with its custom hook. 75 | 76 | ### Typescript 77 | 78 | In all my career I have been mostly reluctant to using types on JS. In my first years, I have used strong-typed languages such as Java and C#, so when I've made the switch to dynamically typed languages such as PHP and Javascript that felt like freedom. 79 | 80 | But as years went by on my career, working with lots of humans coding the same codebase has shown me that it is quite a challenge to maintain a good level of cohesion and "understandability" of the code you're reading and writing, so TS helps with that. Not knowing the shape of data you're dealing with can, not only make things harder to reason about, but also help to introduce bugs to the codebase. 81 | 82 | But one of the best parts of using it in this project was that I could catch bugs earlier and suddenly VSCode got superpowers and the auto-complete was meaningfully helping me for the first time. Moving code around has also proven to be such an easier task. 83 | 84 | ### Hand-crafted UI components 85 | 86 | I think one of the things I most love, if not the biggest, is crafting UI components that when you use them you "feel good". Something like "Aaahhh that's so smooth!", I love using websites and apps that offer that kind of experience so I had to be able to not only craft those components but also make them feel smooth. 87 | 88 | The biggest highlights for me are definitely the bar chart and the month picker. For the chart, I wanted to find a way in which it would be pleased how it presented itself, so that's why I decided to transition the bars, so it feels smooth when you're switching between periods. And for the month picker, I wanted to make something that uses the "UI morphing" technique, which I believe is one of the best ways of showing the user that a context is being switched. 89 | 90 | ### It can be used on any viewport 91 | 92 | You can't think of any web app without giving proper mobile support nowadays, so that by itself was already a challenge I had put to myself, to not only think about the UI on a desktop but also a UI that can adapt to any viewport. 93 | 94 | I also had the need of checking the data anytime I want, so no better than having proper mobile support. 95 | 96 | ### Micro-interactions 97 | 98 | This is another aspect that I absolutely love about doing UI development. Handling all user micro-interactions in a smooth way is something I love to see and to do, so I tried to very attention to those details. 99 | 100 | Also, I really like creating UIs that can handle all sorts of states in a more meaningful way. What I mean by that is that when something errors, I don't want to show a fucking pop-up saying "something went wrong" or if something is loading I don't want to show a page-filling spinner. 101 | 102 | I believe UIs are much more pleasant and clear when the state of a piece of UI can also be shown right where the information is being displayed. That is why I chose to handle all request errors, loading, and missing-data states right where the data is displayed, and by following the challenge of each data component having its query made this task much easier. 103 | 104 | # Features To-do 105 | 106 | There are still some things that I want to create and fine-tune: 107 | - "Add to home screen" support 108 | - Display a nice UI when offline 109 | - Display day events 110 | - Give month picker proper mobile UI layout 111 | - Cover some UI edge-cases (e.g.: displaying something on not-worked days) 112 | 113 | # Project To-do 114 | 115 | There is still much to be done to make this repository more organized: 116 | - More scalable folder structure 117 | - Add instructions of how to run the project to the Readme file 118 | 119 | --- 120 | 121 | Crafted with ♥️ by [@rfviolato](https://twitter.com/rfviolato) 122 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "build": { 4 | "env": { 5 | "FONTAWESOME_NPM_AUTH_TOKEN": "@font-awesome-npm-token" 6 | } 7 | }, 8 | "env": { 9 | "DB_NAME": "@db-name", 10 | "DB_USERNAME": "@db-username", 11 | "DB_PASSWORD": "@db-password" 12 | }, 13 | "builds": [ 14 | { "src": "server/graphql/index.ts", "use": "@now/node" }, 15 | { "src": "server/api/**/*.ts", "use": "@now/node" }, 16 | { 17 | "src": "webapp/package.json", 18 | "use": "@now/static-build", 19 | "config": { "distDir": "build" } 20 | } 21 | ], 22 | "routes": [ 23 | { 24 | "src": "/gql", 25 | "dest": "/server/graphql/index.ts" 26 | }, 27 | { 28 | "src": "/api/(.*)", 29 | "dest": "/server/api/$1.ts" 30 | }, 31 | { 32 | "src": "/static/(.*)", 33 | "headers": { "cache-control": "s-maxage=31536000,immutable" }, 34 | "dest": "/webapp/static/$1" 35 | }, 36 | { "src": "/favicon.ico", "dest": "/webapp/public/favicon.ico" }, 37 | { 38 | "src": "/asset-manifest.json", 39 | "dest": "/webapp/asset-manifest.json" 40 | }, 41 | { "src": "/manifest.json", "dest": "/webapp/manifest.json" }, 42 | { 43 | "src": "/precache-manifest.(.*)", 44 | "dest": "/webapp/precache-manifest.$1" 45 | }, 46 | { 47 | "src": "/service-worker.js", 48 | "headers": { "cache-control": "s-maxage=0" }, 49 | "dest": "/webapp/service-worker.js" 50 | }, 51 | { 52 | "src": "/sockjs-node/(.*)", 53 | "headers": { "cache-control": "s-maxage=0" }, 54 | "dest": "/webapp/sockjs-node/$1" 55 | }, 56 | { 57 | "src": "/__webpack_dev_server__/(.*)", 58 | "headers": { "cache-control": "s-maxage=0" }, 59 | "dest": "/webapp/__webpack_dev_server__/$1" 60 | }, 61 | { 62 | "src": "/(.*).hot-update.js.map", 63 | "headers": { "cache-control": "s-maxage=0" }, 64 | "dest": "/webapp/$1.hot-update.js.map" 65 | }, 66 | { 67 | "src": "/(.*).hot-update.js", 68 | "headers": { "cache-control": "s-maxage=0" }, 69 | "dest": "/webapp/$1.hot-update.js" 70 | }, 71 | { 72 | "src": "/(.*).hot-update.json", 73 | "headers": { "cache-control": "s-maxage=0" }, 74 | "dest": "/webapp/$1.hot-update.json" 75 | }, 76 | { 77 | "src": "/(.*)", 78 | "headers": { "cache-control": "s-maxage=0" }, 79 | "dest": "/webapp/index.html" 80 | } 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "work-commute", 3 | "version": "0.1.0", 4 | "private": true, 5 | "prettier": { 6 | "singleQuote": true, 7 | "arrowParens": "always", 8 | "trailingComma": "all" 9 | }, 10 | "scripts": { 11 | "deploy": "now --prod", 12 | "dump-prod-db": "mongoexport --host Cluster0-shard-0/cluster0-shard-00-00-uk48d.mongodb.net:27017,cluster0-shard-00-01-uk48d.mongodb.net:27017,cluster0-shard-00-02-uk48d.mongodb.net:27017 --ssl --username $DB_USERNAME --password $DB_PASSWORD --authenticationDatabase admin --db work-commute --collection workTimetable --type json --out prod_dump.json", 13 | "populate-dev-db": "mongoimport --host Cluster0-shard-0/cluster0-shard-00-00-uk48d.mongodb.net:27017,cluster0-shard-00-01-uk48d.mongodb.net:27017,cluster0-shard-00-02-uk48d.mongodb.net:27017 --ssl --username $DB_USERNAME --password $DB_PASSWORD --authenticationDatabase admin --db work-commute-dev --collection workTimetable --type json --file ./prod_dump.json" 14 | }, 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "prettier": "^1.19.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /readme/illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/readme/illustration.png -------------------------------------------------------------------------------- /readme/shortcuts_1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/readme/shortcuts_1.jpeg -------------------------------------------------------------------------------- /readme/webapp_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/readme/webapp_1.png -------------------------------------------------------------------------------- /readme/webapp_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/readme/webapp_2.png -------------------------------------------------------------------------------- /server/api/log-event.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from '@now/node'; 2 | import moment from 'moment'; 3 | import { createDbClient } from './../lib/db'; 4 | import { DAY_FORMAT } from '../constants'; 5 | 6 | export default async (request: NowRequest, response: NowResponse) => { 7 | const { 8 | body: { event, date }, 9 | } = request; 10 | const momentDate = moment(date).utc(); 11 | 12 | if (!event) { 13 | return response.status(400).send({ error: 'Date is missing' }); 14 | } 15 | 16 | if (!date) { 17 | return response.status(400).send({ error: 'Event is missing' }); 18 | } 19 | 20 | try { 21 | const db = await createDbClient(); 22 | const day = momentDate.format(DAY_FORMAT); 23 | 24 | await db.workTimetable.updateOne( 25 | { day: { $eq: day } }, 26 | { 27 | $push: { events: event }, 28 | $setOnInsert: { 29 | date: new Date(momentDate.toISOString()), 30 | day, 31 | }, 32 | }, 33 | { upsert: true }, 34 | ); 35 | 36 | return response.status(200).end(); 37 | } catch (exception) { 38 | return response.status(500).send({ error: exception }); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /server/api/log-home-arrive.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from '@now/node'; 2 | import { logTime } from '../utils/log-time'; 3 | 4 | export default async (request: NowRequest, response: NowResponse) => { 5 | const { 6 | body: { date }, 7 | } = request; 8 | 9 | if (!date) { 10 | return response.status(400).send({ error: 'Date is missing' }); 11 | } 12 | 13 | try { 14 | await logTime(date, 'homeArriveTime'); 15 | 16 | return response.status(200).end(); 17 | } catch (exception) { 18 | return response.status(500).send({ error: exception }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /server/api/log-home-leave.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from '@now/node'; 2 | import { logTime } from '../utils/log-time'; 3 | 4 | export default async (request: NowRequest, response: NowResponse) => { 5 | const { 6 | body: { date }, 7 | } = request; 8 | 9 | if (!date) { 10 | return response.status(400).send({ error: 'Date is missing' }); 11 | } 12 | 13 | try { 14 | await logTime(date, 'homeLeaveTime'); 15 | 16 | return response.status(200).end(); 17 | } catch (exception) { 18 | return response.status(500).send({ error: exception }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /server/api/log-work-arrive.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from '@now/node'; 2 | import { logTime } from '../utils/log-time'; 3 | 4 | export default async (request: NowRequest, response: NowResponse) => { 5 | const { 6 | body: { date }, 7 | } = request; 8 | 9 | if (!date) { 10 | return response.status(400).send({ error: 'Date is missing' }); 11 | } 12 | 13 | try { 14 | await logTime(date, 'workArriveTime'); 15 | 16 | return response.status(200).end(); 17 | } catch (exception) { 18 | return response.status(500).send({ error: exception }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /server/api/log-work-leave.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from '@now/node'; 2 | import { logTime } from '../utils/log-time'; 3 | 4 | export default async (request: NowRequest, response: NowResponse) => { 5 | const { 6 | body: { date }, 7 | } = request; 8 | 9 | if (!date) { 10 | return response.status(400).send({ error: 'Date is missing' }); 11 | } 12 | 13 | try { 14 | await logTime(date, 'workLeaveTime'); 15 | 16 | return response.status(200).end(); 17 | } catch (exception) { 18 | return response.status(500).send({ error: exception }); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /server/constants.ts: -------------------------------------------------------------------------------- 1 | export const TIMETABLE_REF = 'workDayTimetables'; 2 | export const DAY_FORMAT = 'YYYY-MM-DD'; 3 | export const TIME_FORMAT = 'HH:mm:ssZ'; 4 | export const FULL_DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; 5 | -------------------------------------------------------------------------------- /server/graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from 'apollo-server-micro'; 2 | import typeDefs from './type-defs'; 3 | import resolvers from './resolvers'; 4 | import { createDbClient } from '../lib/db'; 5 | 6 | const server = new ApolloServer({ 7 | typeDefs, 8 | resolvers, 9 | context: async () => { 10 | const db = await createDbClient(); 11 | 12 | return { db }; 13 | }, 14 | introspection: true, 15 | playground: true, 16 | }); 17 | 18 | export const config = { 19 | api: { 20 | bodyParser: false, 21 | }, 22 | }; 23 | 24 | export default server.createHandler({ path: '/gql' }); 25 | -------------------------------------------------------------------------------- /server/graphql/interface.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from 'mongodb'; 2 | import { ITime, IDayTimetableRecord } from '../interfaces'; 3 | 4 | export interface ITotalEveningCommuteResolverResult extends ITime {} 5 | export interface ITotalMorningCommuteResolverResult extends ITime {} 6 | export interface ITotalTimeAtOfficeResolverResult extends ITime {} 7 | export interface IAverageTimeCommutingResolverResult extends ITime {} 8 | export interface IAverageTimeAtOfficeResolverResult extends ITime {} 9 | 10 | interface IChartProcessedData { 11 | totalMorningCommuteTime: ITotalMorningCommuteResolverResult; 12 | totalEveningCommuteTime: ITotalEveningCommuteResolverResult; 13 | totalTimeAtOffice: ITotalTimeAtOfficeResolverResult; 14 | } 15 | 16 | export type ITimetableChartResolverResult = (IDayTimetableRecord & 17 | IChartProcessedData)[]; 18 | 19 | export interface IGQLContext { 20 | db: { 21 | workTimetable: Collection; 22 | }; 23 | } 24 | 25 | export interface IPeriodQueryParams { 26 | periodStart: string; 27 | periodEnd: string; 28 | } 29 | 30 | export interface IDayQueryParams { 31 | day: string; 32 | } 33 | -------------------------------------------------------------------------------- /server/graphql/resolvers/day/index.ts: -------------------------------------------------------------------------------- 1 | import totalMorningCommuteTime from './total-morning-commute-time'; 2 | import totalEveningCommuteTime from './total-evening-commute-time'; 3 | import totalTimeAtOffice from './total-time-at-office'; 4 | 5 | export default { 6 | totalMorningCommuteTime, 7 | totalEveningCommuteTime, 8 | totalTimeAtOffice, 9 | }; 10 | -------------------------------------------------------------------------------- /server/graphql/resolvers/day/total-evening-commute-time.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { TIME_FORMAT } from '../../../constants'; 3 | import { getTimeFromMinutes } from '../../../utils/get-time-from-minutes'; 4 | import { IDayTimetableRecord } from '../../../interfaces'; 5 | import { ITotalEveningCommuteResolverResult } from '../../interface'; 6 | 7 | export default ({ 8 | workLeaveTime, 9 | homeArriveTime, 10 | }: IDayTimetableRecord): ITotalEveningCommuteResolverResult => { 11 | try { 12 | if (workLeaveTime && homeArriveTime) { 13 | const workLeaveTimeDate = moment(workLeaveTime, TIME_FORMAT); 14 | const homeArriveTimeDate = moment(homeArriveTime, TIME_FORMAT); 15 | const totalMinutesCommuting = homeArriveTimeDate.diff( 16 | workLeaveTimeDate, 17 | 'minutes', 18 | ); 19 | 20 | return getTimeFromMinutes(totalMinutesCommuting); 21 | } 22 | 23 | return getTimeFromMinutes(0); 24 | } catch (e) { 25 | throw new Error(e); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /server/graphql/resolvers/day/total-morning-commute-time.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { TIME_FORMAT } from '../../../constants'; 3 | import { getTimeFromMinutes } from '../../../utils/get-time-from-minutes'; 4 | import { IDayTimetableRecord } from '../../../interfaces'; 5 | import { ITotalMorningCommuteResolverResult } from '../../interface'; 6 | 7 | export default ({ 8 | homeLeaveTime, 9 | workArriveTime, 10 | }: IDayTimetableRecord): ITotalMorningCommuteResolverResult => { 11 | try { 12 | if (homeLeaveTime && workArriveTime) { 13 | const homeLeaveTimeDate = moment(homeLeaveTime, TIME_FORMAT); 14 | const workArriveTimeDate = moment(workArriveTime, TIME_FORMAT); 15 | const totalMinutesCommuting = workArriveTimeDate.diff( 16 | homeLeaveTimeDate, 17 | 'minutes', 18 | ); 19 | 20 | return getTimeFromMinutes(totalMinutesCommuting); 21 | } 22 | 23 | return getTimeFromMinutes(0); 24 | } catch (e) { 25 | throw new Error(e); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /server/graphql/resolvers/day/total-time-at-office.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { getTimeFromMinutes } from '../../../utils/get-time-from-minutes'; 3 | import { TIME_FORMAT } from '../../../constants'; 4 | import { IDayTimetableRecord } from '../../../interfaces'; 5 | import { ITotalTimeAtOfficeResolverResult } from '../../interface'; 6 | 7 | export default ({ 8 | workArriveTime, 9 | workLeaveTime, 10 | }: IDayTimetableRecord): ITotalTimeAtOfficeResolverResult => { 11 | try { 12 | if (workArriveTime && workLeaveTime) { 13 | const workArriveDate = moment(workArriveTime, TIME_FORMAT); 14 | 15 | const workLeaveDate = moment(workLeaveTime, TIME_FORMAT); 16 | 17 | return getTimeFromMinutes(workLeaveDate.diff(workArriveDate, 'minutes')); 18 | } 19 | 20 | if (workArriveTime && !workLeaveTime) { 21 | const workArriveDate = moment(workArriveTime, TIME_FORMAT); 22 | const workLeaveDate = moment(); 23 | 24 | return getTimeFromMinutes(workLeaveDate.diff(workArriveDate, 'minutes')); 25 | } 26 | 27 | return getTimeFromMinutes(0); 28 | } catch (e) { 29 | throw new Error(e); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /server/graphql/resolvers/index.ts: -------------------------------------------------------------------------------- 1 | import { IResolvers } from 'apollo-server-micro'; 2 | import Query from './query'; 3 | import Period from './period'; 4 | import Day from './day'; 5 | import { IGQLContext } from './../interface'; 6 | 7 | export default { 8 | Query, 9 | Period, 10 | Day, 11 | } as IResolvers; 12 | -------------------------------------------------------------------------------- /server/graphql/resolvers/period/average-time-at-office.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { TIME_FORMAT } from '../../../constants'; 3 | import { getTimeFromMinutes } from '../../../utils/get-time-from-minutes'; 4 | import { IDayTimetableRecord } from '../../../interfaces'; 5 | import { IAverageTimeAtOfficeResolverResult } from '../../interface'; 6 | 7 | export default ( 8 | timetables: IDayTimetableRecord[], 9 | ): IAverageTimeAtOfficeResolverResult => { 10 | try { 11 | const result = timetables.reduce( 12 | (accum, { workArriveTime, workLeaveTime, day }) => { 13 | if (workArriveTime && workLeaveTime) { 14 | const workArriveDate = moment(workArriveTime, TIME_FORMAT); 15 | const workLeaveDate = moment(workLeaveTime, TIME_FORMAT); 16 | const minutesAtTheOffice = workLeaveDate.diff( 17 | workArriveDate, 18 | 'minutes', 19 | ); 20 | 21 | accum.workdayCount++; 22 | accum.minutesAtTheOffice += minutesAtTheOffice; 23 | } 24 | 25 | return accum; 26 | }, 27 | { minutesAtTheOffice: 0, workdayCount: 0 }, 28 | ); 29 | 30 | const averageMinutesAtOffice = 31 | result.minutesAtTheOffice / result.workdayCount; 32 | 33 | if (isNaN(averageMinutesAtOffice) || averageMinutesAtOffice === Infinity) { 34 | return getTimeFromMinutes(0); 35 | } 36 | 37 | return getTimeFromMinutes(averageMinutesAtOffice); 38 | } catch (e) { 39 | throw new Error(e); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /server/graphql/resolvers/period/average-time-commuting.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { TIME_FORMAT } from '../../../constants'; 3 | import { getTimeFromMinutes } from './../../../utils/get-time-from-minutes'; 4 | import { IDayTimetableRecord } from '../../../interfaces'; 5 | import { IAverageTimeCommutingResolverResult } from '../../interface'; 6 | 7 | export default ( 8 | timetables: IDayTimetableRecord[], 9 | ): IAverageTimeCommutingResolverResult => { 10 | try { 11 | const result = timetables.reduce( 12 | ( 13 | accum, 14 | { homeLeaveTime, workArriveTime, workLeaveTime, homeArriveTime }, 15 | ) => { 16 | const hasMorningCommute = homeLeaveTime && workArriveTime; 17 | const hasEveningCommute = workLeaveTime && homeArriveTime; 18 | 19 | if (hasMorningCommute) { 20 | const homeLeaveDate = moment(homeLeaveTime, TIME_FORMAT); 21 | const workArriveDate = moment(workArriveTime, TIME_FORMAT); 22 | const morningCommuteMinutes = workArriveDate.diff( 23 | homeLeaveDate, 24 | 'minutes', 25 | ); 26 | 27 | accum.minutesCommuting += morningCommuteMinutes; 28 | accum.commuteCount++; 29 | } 30 | 31 | if (hasEveningCommute) { 32 | const workLeaveDate = moment(workLeaveTime, TIME_FORMAT); 33 | const homeArriveDate = moment(homeArriveTime, TIME_FORMAT); 34 | const eveningCommuteInMinutes = homeArriveDate.diff( 35 | workLeaveDate, 36 | 'minutes', 37 | ); 38 | 39 | accum.minutesCommuting += eveningCommuteInMinutes; 40 | accum.commuteCount++; 41 | } 42 | 43 | return accum; 44 | }, 45 | { minutesCommuting: 0, commuteCount: 0 }, 46 | ); 47 | 48 | const averageMinutesCommuting = 49 | result.minutesCommuting / result.commuteCount; 50 | 51 | if ( 52 | isNaN(averageMinutesCommuting) || 53 | averageMinutesCommuting === Infinity 54 | ) { 55 | return getTimeFromMinutes(0); 56 | } 57 | 58 | return getTimeFromMinutes(averageMinutesCommuting); 59 | } catch (e) { 60 | throw new Error(e); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /server/graphql/resolvers/period/index.ts: -------------------------------------------------------------------------------- 1 | import totalTimeAtOffice from './total-time-at-office'; 2 | import averageTimeCommuting from './average-time-commuting'; 3 | import averageTimeAtOffice from './average-time-at-office'; 4 | import timetableChart from './timetable-chart'; 5 | 6 | export default { 7 | totalTimeAtOffice, 8 | averageTimeCommuting, 9 | averageTimeAtOffice, 10 | timetableChart, 11 | }; 12 | -------------------------------------------------------------------------------- /server/graphql/resolvers/period/timetable-chart.ts: -------------------------------------------------------------------------------- 1 | import { IDayTimetableRecord } from '../../../interfaces'; 2 | import getTotalTimeAtOffice from './../day/total-time-at-office'; 3 | import getTotalMorningCommuteTime from './../day/total-morning-commute-time'; 4 | import getTotalEveningCommuteTime from './../day/total-evening-commute-time'; 5 | import { ITimetableChartResolverResult } from '../../interface'; 6 | 7 | export default ( 8 | timetables: IDayTimetableRecord[], 9 | ): ITimetableChartResolverResult => { 10 | try { 11 | return timetables.map((timetable) => { 12 | const totalMorningCommuteTime = getTotalMorningCommuteTime(timetable); 13 | const totalEveningCommuteTime = getTotalEveningCommuteTime(timetable); 14 | 15 | return { 16 | ...timetable, 17 | totalTimeAtOffice: getTotalTimeAtOffice(timetable), 18 | totalMorningCommuteTime, 19 | totalEveningCommuteTime, 20 | }; 21 | }); 22 | } catch (e) { 23 | throw new Error(e); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /server/graphql/resolvers/period/total-time-at-office.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import { TIME_FORMAT } from '../../../constants'; 3 | import { getTimeFromMinutes } from '../../../utils/get-time-from-minutes'; 4 | import { IDayTimetableRecord } from '../../../interfaces'; 5 | import { ITotalTimeAtOfficeResolverResult } from '../../interface'; 6 | 7 | export default ( 8 | timetables: IDayTimetableRecord[], 9 | ): ITotalTimeAtOfficeResolverResult => { 10 | try { 11 | const totalMinutesAtOffice = timetables.reduce( 12 | (accum, { workArriveTime, workLeaveTime, day }) => { 13 | if (workArriveTime && workLeaveTime) { 14 | const workLeaveDate = moment(workLeaveTime, TIME_FORMAT); 15 | 16 | const workArriveDate = moment(workArriveTime, TIME_FORMAT); 17 | 18 | return (accum += workLeaveDate.diff(workArriveDate, 'minutes')); 19 | } 20 | 21 | return accum; 22 | }, 23 | 0, 24 | ); 25 | 26 | return getTimeFromMinutes(totalMinutesAtOffice); 27 | } catch (e) { 28 | throw new Error(e); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /server/graphql/resolvers/query/day.ts: -------------------------------------------------------------------------------- 1 | import { IGQLContext, IDayQueryParams } from './../../interface'; 2 | 3 | export default async ( 4 | parent: any, 5 | { day }: IDayQueryParams, 6 | { db }: IGQLContext, 7 | ): Promise => { 8 | try { 9 | return await db.workTimetable.findOne({ 10 | day: { 11 | $eq: day, 12 | }, 13 | }); 14 | } catch (e) { 15 | throw new Error(e); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /server/graphql/resolvers/query/first-record-info.ts: -------------------------------------------------------------------------------- 1 | import { IGQLContext } from '../../interface'; 2 | 3 | export default async ( 4 | parent: any, 5 | params: any, 6 | { db }: IGQLContext, 7 | ): Promise => { 8 | try { 9 | const result = await db.workTimetable 10 | .find() 11 | .sort({ date: 1 }) 12 | .limit(1) 13 | .toArray(); 14 | 15 | return result[0]; 16 | } catch (e) { 17 | throw new Error(e); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /server/graphql/resolvers/query/index.ts: -------------------------------------------------------------------------------- 1 | import Period from './period'; 2 | import Day from './day'; 3 | import FirstRecord from './first-record-info'; 4 | 5 | export default { 6 | Period, 7 | Day, 8 | FirstRecord, 9 | }; 10 | -------------------------------------------------------------------------------- /server/graphql/resolvers/query/period.ts: -------------------------------------------------------------------------------- 1 | import { IGQLContext, IPeriodQueryParams } from './../../interface'; 2 | 3 | export default async ( 4 | parent: any, 5 | { periodStart, periodEnd }: IPeriodQueryParams, 6 | { db }: IGQLContext, 7 | ) => { 8 | try { 9 | return await db.workTimetable 10 | .find({ 11 | date: { 12 | $gte: new Date(periodStart), 13 | $lte: new Date(periodEnd), 14 | }, 15 | }) 16 | .sort({ date: 1 }) 17 | .toArray(); 18 | } catch (e) { 19 | throw new Error(e); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /server/graphql/type-defs.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-micro'; 2 | import { DocumentNode } from 'graphql'; 3 | 4 | export default gql` 5 | type Query { 6 | Period(periodStart: String!, periodEnd: String!): Period 7 | Day(day: String!): Day 8 | FirstRecord: WorkTimetableRecord 9 | } 10 | 11 | type PeriodData { 12 | periodStart: String 13 | periodEnd: String 14 | } 15 | 16 | type Period { 17 | totalTimeAtOffice: TotalTimeAtOfficeResult 18 | averageTimeCommuting: AverageTimeCommutingResult 19 | averageTimeAtOffice: AverageTimeAtOfficeResult 20 | timetableChart: [TimetableChartResult] 21 | } 22 | 23 | type Day { 24 | date: String 25 | day: String 26 | events: [String] 27 | homeArriveTime: String 28 | homeLeaveTime: String 29 | workArriveTime: String 30 | workLeaveTime: String 31 | totalMorningCommuteTime: TotalMorningCommuteTime 32 | totalEveningCommuteTime: TotalEveningCommuteTime 33 | totalTimeAtOffice: TotalTimeAtOfficeResult 34 | } 35 | 36 | type TimetableChartResult { 37 | date: String 38 | day: String 39 | events: [String] 40 | homeArriveTime: String 41 | homeLeaveTime: String 42 | workArriveTime: String 43 | workLeaveTime: String 44 | totalTimeAtOffice: TotalTimeAtOfficeResult 45 | totalMorningCommuteTime: TotalMorningCommuteTime 46 | totalEveningCommuteTime: TotalEveningCommuteTime 47 | } 48 | 49 | type WorkTimetableRecord { 50 | date: String 51 | day: String 52 | homeArriveTime: String 53 | homeLeaveTime: String 54 | workArriveTime: String 55 | workLeaveTime: String 56 | events: [String] 57 | } 58 | 59 | type TotalTimeAtOfficeResult { 60 | hours: Int 61 | minutes: Int 62 | } 63 | 64 | type AverageTimeCommutingResult { 65 | hours: Int 66 | minutes: Int 67 | } 68 | 69 | type AverageTimeAtOfficeResult { 70 | hours: Int 71 | minutes: Int 72 | } 73 | 74 | type TotalMorningCommuteTime { 75 | hours: Int 76 | minutes: Int 77 | } 78 | 79 | type TotalEveningCommuteTime { 80 | hours: Int 81 | minutes: Int 82 | } 83 | ` as DocumentNode; 84 | -------------------------------------------------------------------------------- /server/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Collection } from 'mongodb'; 2 | 3 | export interface ITime { 4 | hours: number; 5 | minutes: number; 6 | } 7 | 8 | export interface IDB { 9 | workTimetable: Collection; 10 | } 11 | 12 | export interface IDayTimetableRecord { 13 | _id: string; 14 | date: string; 15 | day: string; 16 | homeArriveTime: string; 17 | homeLeaveTime: string; 18 | workArriveTime: string; 19 | workLeaveTime: string; 20 | events: { 21 | [key: string]: string; 22 | }[]; 23 | } 24 | -------------------------------------------------------------------------------- /server/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { IDB } from '../interfaces'; 3 | 4 | const username = process.env.DB_USERNAME; 5 | const password = process.env.DB_PASSWORD; 6 | const MONGODB_URL = `mongodb+srv://${username}:${password}@cluster0-uk48d.mongodb.net/work-commute?retryWrites=true&w=majority`; 7 | 8 | export const createDbClient = async (): Promise => { 9 | try { 10 | const client = await MongoClient.connect(MONGODB_URL, { 11 | useUnifiedTopology: true, 12 | }); 13 | 14 | const db = client.db(process.env.DB_NAME); 15 | const workTimetable = db.collection('workTimetable'); 16 | 17 | return { 18 | workTimetable, 19 | }; 20 | } catch (e) { 21 | throw new Error(e); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "description": "work-commut server", 4 | "dependencies": { 5 | "@now/node": "^1.1.2", 6 | "@types/mongodb": "^3.3.11", 7 | "@types/mongoose": "^5.5.32", 8 | "@types/moment": "^2.13.0", 9 | "apollo-server-micro": "^2.9.8", 10 | "firebase-admin": "^8.7.0", 11 | "graphql": "^14.5.8", 12 | "moment": "^2.24.0", 13 | "mongodb": "^3.3.4", 14 | "mongoose": "^5.7.12", 15 | "typescript": "^3.7.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "lib": ["es6", "dom"], 6 | "outDir": "lib", 7 | "strict": true, 8 | "typeRoots": ["node_modules/@types"], 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "moduleResolution": "node", 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /server/utils/get-time-from-minutes.ts: -------------------------------------------------------------------------------- 1 | import { ITime } from '../interfaces'; 2 | 3 | export const getTimeFromMinutes = (totalMinutes: number): ITime => { 4 | const hours = Math.floor(totalMinutes / 60); 5 | const minutes = Math.floor(totalMinutes % 60); 6 | 7 | return { 8 | hours, 9 | minutes, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /server/utils/log-time.ts: -------------------------------------------------------------------------------- 1 | import { UpdateWriteOpResult } from 'mongodb'; 2 | import moment from 'moment'; 3 | import { TIME_FORMAT, DAY_FORMAT } from '../constants'; 4 | import { createDbClient } from '../lib/db'; 5 | 6 | export const logTime = async ( 7 | date: string, 8 | property: string, 9 | ): Promise => { 10 | const momentDate = moment(date).utc(); 11 | const day = momentDate.format(DAY_FORMAT); 12 | const time = momentDate.format(TIME_FORMAT); 13 | const db = await createDbClient(); 14 | 15 | return db.workTimetable.updateOne( 16 | { day: { $eq: day } }, 17 | { 18 | $set: { [property]: time }, 19 | $setOnInsert: { 20 | date: new Date(momentDate.toISOString()), 21 | day, 22 | }, 23 | }, 24 | { upsert: true }, 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@apollo/protobufjs@^1.0.3": 6 | version "1.0.4" 7 | resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.0.4.tgz#cf01747a55359066341f31b5ce8db17df44244e0" 8 | dependencies: 9 | "@protobufjs/aspromise" "^1.1.2" 10 | "@protobufjs/base64" "^1.1.2" 11 | "@protobufjs/codegen" "^2.0.4" 12 | "@protobufjs/eventemitter" "^1.1.0" 13 | "@protobufjs/fetch" "^1.1.0" 14 | "@protobufjs/float" "^1.0.2" 15 | "@protobufjs/inquire" "^1.1.0" 16 | "@protobufjs/path" "^1.1.2" 17 | "@protobufjs/pool" "^1.1.0" 18 | "@protobufjs/utf8" "^1.1.0" 19 | "@types/long" "^4.0.0" 20 | "@types/node" "^10.1.0" 21 | long "^4.0.0" 22 | 23 | "@apollographql/apollo-tools@^0.4.3": 24 | version "0.4.8" 25 | resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.4.8.tgz#d81da89ee880c2345eb86bddb92b35291f6135ed" 26 | dependencies: 27 | apollo-env "^0.6.5" 28 | 29 | "@apollographql/graphql-playground-html@1.6.24": 30 | version "1.6.24" 31 | resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" 32 | 33 | "@firebase/app-types@0.4.8": 34 | version "0.4.8" 35 | resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.8.tgz#be69cbf3a7550c900d7af943adb2a3d1dcce6631" 36 | 37 | "@firebase/database-types@0.4.8": 38 | version "0.4.8" 39 | resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.8.tgz#ec225ac9e37a31cb0aa827b86d0073dfdc5289f1" 40 | dependencies: 41 | "@firebase/app-types" "0.4.8" 42 | 43 | "@firebase/database@^0.5.11": 44 | version "0.5.13" 45 | resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.5.13.tgz#1d8296d15efefeafe26877f9753fe1f283a0ec63" 46 | dependencies: 47 | "@firebase/database-types" "0.4.8" 48 | "@firebase/logger" "0.1.31" 49 | "@firebase/util" "0.2.34" 50 | faye-websocket "0.11.3" 51 | tslib "1.10.0" 52 | 53 | "@firebase/logger@0.1.31": 54 | version "0.1.31" 55 | resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.31.tgz#e0ab28af14333786952d7a5154f90d0453414d24" 56 | 57 | "@firebase/util@0.2.34": 58 | version "0.2.34" 59 | resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.34.tgz#a84fc09a68e82012b650964944e8ffc956ec4912" 60 | dependencies: 61 | tslib "1.10.0" 62 | 63 | "@google-cloud/common@^2.1.1": 64 | version "2.2.3" 65 | resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-2.2.3.tgz#fc29701b09d7bc9d26ea6b6be119c77485210dbf" 66 | dependencies: 67 | "@google-cloud/projectify" "^1.0.0" 68 | "@google-cloud/promisify" "^1.0.0" 69 | arrify "^2.0.0" 70 | duplexify "^3.6.0" 71 | ent "^2.2.0" 72 | extend "^3.0.2" 73 | google-auth-library "^5.5.0" 74 | retry-request "^4.0.0" 75 | teeny-request "^5.2.1" 76 | 77 | "@google-cloud/firestore@^2.6.0": 78 | version "2.6.0" 79 | resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-2.6.0.tgz#9d60bc405212a6460f748fe3efdfbd2b6c1cbbe6" 80 | dependencies: 81 | bun "^0.0.12" 82 | deep-equal "^1.0.1" 83 | functional-red-black-tree "^1.0.1" 84 | google-gax "^1.7.5" 85 | through2 "^3.0.0" 86 | 87 | "@google-cloud/paginator@^2.0.0": 88 | version "2.0.2" 89 | resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-2.0.2.tgz#fdcfbe93bf037f3caa0c678e962efd0382062417" 90 | dependencies: 91 | arrify "^2.0.0" 92 | extend "^3.0.2" 93 | 94 | "@google-cloud/projectify@^1.0.0": 95 | version "1.0.2" 96 | resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-1.0.2.tgz#8ea0af54459bb7c94e0cd69d1ab64f3380ae164d" 97 | 98 | "@google-cloud/promisify@^1.0.0": 99 | version "1.0.3" 100 | resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-1.0.3.tgz#b947ff3b8f9dd9248c57d1b2133764a7bb42a6c5" 101 | 102 | "@google-cloud/storage@^4.1.2": 103 | version "4.1.3" 104 | resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-4.1.3.tgz#f570ed06fcf51834a4761d01aaa3fb81570f32f6" 105 | dependencies: 106 | "@google-cloud/common" "^2.1.1" 107 | "@google-cloud/paginator" "^2.0.0" 108 | "@google-cloud/promisify" "^1.0.0" 109 | arrify "^2.0.0" 110 | compressible "^2.0.12" 111 | concat-stream "^2.0.0" 112 | date-and-time "^0.11.0" 113 | duplexify "^3.5.0" 114 | extend "^3.0.2" 115 | gaxios "^2.0.1" 116 | gcs-resumable-upload "^2.2.4" 117 | hash-stream-validation "^0.2.2" 118 | mime "^2.2.0" 119 | mime-types "^2.0.8" 120 | onetime "^5.1.0" 121 | p-limit "^2.2.0" 122 | pumpify "^2.0.0" 123 | readable-stream "^3.4.0" 124 | snakeize "^0.1.0" 125 | stream-events "^1.0.1" 126 | through2 "^3.0.0" 127 | xdg-basedir "^4.0.0" 128 | 129 | "@grpc/grpc-js@0.6.9": 130 | version "0.6.9" 131 | resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-0.6.9.tgz#49e0b32b92b822df294cc576df169cc6112063b4" 132 | dependencies: 133 | semver "^6.2.0" 134 | 135 | "@grpc/proto-loader@^0.5.1": 136 | version "0.5.3" 137 | resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.3.tgz#a233070720bf7560c4d70e29e7950c72549a132c" 138 | dependencies: 139 | lodash.camelcase "^4.3.0" 140 | protobufjs "^6.8.6" 141 | 142 | "@now/node@^1.1.2": 143 | version "1.2.0" 144 | resolved "https://registry.yarnpkg.com/@now/node/-/node-1.2.0.tgz#9d8ee325b3fbf9cd453d00419f3cbe7819cb3ac3" 145 | dependencies: 146 | "@types/node" "*" 147 | 148 | "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": 149 | version "1.1.2" 150 | resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" 151 | 152 | "@protobufjs/base64@^1.1.2": 153 | version "1.1.2" 154 | resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" 155 | 156 | "@protobufjs/codegen@^2.0.4": 157 | version "2.0.4" 158 | resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" 159 | 160 | "@protobufjs/eventemitter@^1.1.0": 161 | version "1.1.0" 162 | resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" 163 | 164 | "@protobufjs/fetch@^1.1.0": 165 | version "1.1.0" 166 | resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" 167 | dependencies: 168 | "@protobufjs/aspromise" "^1.1.1" 169 | "@protobufjs/inquire" "^1.1.0" 170 | 171 | "@protobufjs/float@^1.0.2": 172 | version "1.0.2" 173 | resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" 174 | 175 | "@protobufjs/inquire@^1.1.0": 176 | version "1.1.0" 177 | resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" 178 | 179 | "@protobufjs/path@^1.1.2": 180 | version "1.1.2" 181 | resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" 182 | 183 | "@protobufjs/pool@^1.1.0": 184 | version "1.1.0" 185 | resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" 186 | 187 | "@protobufjs/utf8@^1.1.0": 188 | version "1.1.0" 189 | resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" 190 | 191 | "@types/accepts@*": 192 | version "1.3.5" 193 | resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" 194 | dependencies: 195 | "@types/node" "*" 196 | 197 | "@types/body-parser@*": 198 | version "1.19.0" 199 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" 200 | dependencies: 201 | "@types/connect" "*" 202 | "@types/node" "*" 203 | 204 | "@types/bson@*": 205 | version "4.0.1" 206 | resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.1.tgz#2bfc80819e7055b76d5496d5344ed23e5d12bbb2" 207 | dependencies: 208 | "@types/node" "*" 209 | 210 | "@types/connect@*": 211 | version "3.4.33" 212 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" 213 | dependencies: 214 | "@types/node" "*" 215 | 216 | "@types/content-disposition@*": 217 | version "0.5.3" 218 | resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.3.tgz#0aa116701955c2faa0717fc69cd1596095e49d96" 219 | 220 | "@types/cookies@*": 221 | version "0.7.4" 222 | resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b" 223 | dependencies: 224 | "@types/connect" "*" 225 | "@types/express" "*" 226 | "@types/keygrip" "*" 227 | "@types/node" "*" 228 | 229 | "@types/express-serve-static-core@*": 230 | version "4.17.7" 231 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz#dfe61f870eb549dc6d7e12050901847c7d7e915b" 232 | dependencies: 233 | "@types/node" "*" 234 | "@types/qs" "*" 235 | "@types/range-parser" "*" 236 | 237 | "@types/express@*": 238 | version "4.17.6" 239 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e" 240 | dependencies: 241 | "@types/body-parser" "*" 242 | "@types/express-serve-static-core" "*" 243 | "@types/qs" "*" 244 | "@types/serve-static" "*" 245 | 246 | "@types/fs-capacitor@*": 247 | version "2.0.0" 248 | resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e" 249 | dependencies: 250 | "@types/node" "*" 251 | 252 | "@types/graphql-upload@^8.0.0": 253 | version "8.0.3" 254 | resolved "https://registry.yarnpkg.com/@types/graphql-upload/-/graphql-upload-8.0.3.tgz#b371edb5f305a2a1f7b7843a890a2a7adc55c3ec" 255 | dependencies: 256 | "@types/express" "*" 257 | "@types/fs-capacitor" "*" 258 | "@types/koa" "*" 259 | graphql "^14.5.3" 260 | 261 | "@types/http-assert@*": 262 | version "1.5.1" 263 | resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" 264 | 265 | "@types/keygrip@*": 266 | version "1.0.2" 267 | resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" 268 | 269 | "@types/koa-compose@*": 270 | version "3.2.5" 271 | resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" 272 | dependencies: 273 | "@types/koa" "*" 274 | 275 | "@types/koa@*": 276 | version "2.11.3" 277 | resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.3.tgz#540ece376581b12beadf9a417dd1731bc31c16ce" 278 | dependencies: 279 | "@types/accepts" "*" 280 | "@types/content-disposition" "*" 281 | "@types/cookies" "*" 282 | "@types/http-assert" "*" 283 | "@types/keygrip" "*" 284 | "@types/koa-compose" "*" 285 | "@types/node" "*" 286 | 287 | "@types/long@^4.0.0": 288 | version "4.0.1" 289 | resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" 290 | 291 | "@types/mime@*": 292 | version "2.0.2" 293 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.2.tgz#857a118d8634c84bba7ae14088e4508490cd5da5" 294 | 295 | "@types/moment@^2.13.0": 296 | version "2.13.0" 297 | resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896" 298 | dependencies: 299 | moment "*" 300 | 301 | "@types/mongodb@*", "@types/mongodb@^3.3.11": 302 | version "3.3.11" 303 | resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.3.11.tgz#8db69d44cd3845a8867452c2a9b7f449fe65f1fc" 304 | dependencies: 305 | "@types/bson" "*" 306 | "@types/node" "*" 307 | 308 | "@types/mongoose@^5.5.32": 309 | version "5.5.32" 310 | resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.32.tgz#8a76c5be029086c1225bf88ed3ca83f01181121f" 311 | dependencies: 312 | "@types/mongodb" "*" 313 | "@types/node" "*" 314 | 315 | "@types/node-fetch@2.5.7": 316 | version "2.5.7" 317 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" 318 | dependencies: 319 | "@types/node" "*" 320 | form-data "^3.0.0" 321 | 322 | "@types/node@*": 323 | version "14.0.11" 324 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.11.tgz#61d4886e2424da73b7b25547f59fdcb534c165a3" 325 | 326 | "@types/node@^10.1.0": 327 | version "10.17.24" 328 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.24.tgz#c57511e3a19c4b5e9692bb2995c40a3a52167944" 329 | 330 | "@types/node@^8.0.53": 331 | version "8.10.59" 332 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.59.tgz#9e34261f30183f9777017a13d185dfac6b899e04" 333 | 334 | "@types/qs@*": 335 | version "6.9.3" 336 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.3.tgz#b755a0934564a200d3efdf88546ec93c369abd03" 337 | 338 | "@types/range-parser@*": 339 | version "1.2.3" 340 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" 341 | 342 | "@types/serve-static@*": 343 | version "1.13.4" 344 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.4.tgz#6662a93583e5a6cabca1b23592eb91e12fa80e7c" 345 | dependencies: 346 | "@types/express-serve-static-core" "*" 347 | "@types/mime" "*" 348 | 349 | "@types/ws@^7.0.0": 350 | version "7.2.5" 351 | resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49" 352 | dependencies: 353 | "@types/node" "*" 354 | 355 | "@wry/equality@^0.1.2": 356 | version "0.1.11" 357 | resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.11.tgz#35cb156e4a96695aa81a9ecc4d03787bc17f1790" 358 | dependencies: 359 | tslib "^1.9.3" 360 | 361 | abort-controller@^3.0.0: 362 | version "3.0.0" 363 | resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" 364 | dependencies: 365 | event-target-shim "^5.0.0" 366 | 367 | accept@^3.0.2: 368 | version "3.1.3" 369 | resolved "https://registry.yarnpkg.com/accept/-/accept-3.1.3.tgz#29c3e2b3a8f4eedbc2b690e472b9ebbdc7385e87" 370 | dependencies: 371 | boom "7.x.x" 372 | hoek "6.x.x" 373 | 374 | agent-base@4, agent-base@^4.3.0: 375 | version "4.3.0" 376 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" 377 | dependencies: 378 | es6-promisify "^5.0.0" 379 | 380 | apollo-cache-control@^0.11.0: 381 | version "0.11.0" 382 | resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.11.0.tgz#7075492d04c5424e7c6769380b503e8f75b39d61" 383 | dependencies: 384 | apollo-server-env "^2.4.4" 385 | apollo-server-plugin-base "^0.9.0" 386 | 387 | apollo-datasource@^0.7.1: 388 | version "0.7.1" 389 | resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.1.tgz#0b06da999ace50b7f5fe509f2a03f7de97974334" 390 | dependencies: 391 | apollo-server-caching "^0.5.1" 392 | apollo-server-env "^2.4.4" 393 | 394 | apollo-engine-reporting-protobuf@^0.4.4: 395 | version "0.4.4" 396 | resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.4.tgz#73a064f8c9f2d6605192d1673729c66ec47d9cb7" 397 | dependencies: 398 | "@apollo/protobufjs" "^1.0.3" 399 | 400 | apollo-engine-reporting-protobuf@^0.5.1: 401 | version "0.5.1" 402 | resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.1.tgz#b6e66e6e382f9bcdc2ac8ed168b047eb1470c1a8" 403 | dependencies: 404 | "@apollo/protobufjs" "^1.0.3" 405 | 406 | apollo-engine-reporting@^2.0.0: 407 | version "2.0.0" 408 | resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-2.0.0.tgz#af007b4a8a481fa97baef0eac51a7824f1ec3310" 409 | dependencies: 410 | apollo-engine-reporting-protobuf "^0.5.1" 411 | apollo-graphql "^0.4.0" 412 | apollo-server-caching "^0.5.1" 413 | apollo-server-env "^2.4.4" 414 | apollo-server-errors "^2.4.1" 415 | apollo-server-plugin-base "^0.9.0" 416 | apollo-server-types "^0.5.0" 417 | async-retry "^1.2.1" 418 | uuid "^8.0.0" 419 | 420 | apollo-env@^0.6.5: 421 | version "0.6.5" 422 | resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.6.5.tgz#5a36e699d39e2356381f7203493187260fded9f3" 423 | dependencies: 424 | "@types/node-fetch" "2.5.7" 425 | core-js "^3.0.1" 426 | node-fetch "^2.2.0" 427 | sha.js "^2.4.11" 428 | 429 | apollo-graphql@^0.4.0: 430 | version "0.4.4" 431 | resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.4.4.tgz#25f456b28a4419bb6a42071f8a56e19e15bb80be" 432 | dependencies: 433 | apollo-env "^0.6.5" 434 | lodash.sortby "^4.7.0" 435 | 436 | apollo-link@^1.2.14: 437 | version "1.2.14" 438 | resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9" 439 | dependencies: 440 | apollo-utilities "^1.3.0" 441 | ts-invariant "^0.4.0" 442 | tslib "^1.9.3" 443 | zen-observable-ts "^0.8.21" 444 | 445 | apollo-server-caching@^0.5.0, apollo-server-caching@^0.5.1: 446 | version "0.5.1" 447 | resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz#5cd0536ad5473abb667cc82b59bc56b96fb35db6" 448 | dependencies: 449 | lru-cache "^5.0.0" 450 | 451 | apollo-server-core@^2.9.12: 452 | version "2.14.2" 453 | resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.14.2.tgz#4ab055b96b8be7821a726c81e8aa412deb7f3644" 454 | dependencies: 455 | "@apollographql/apollo-tools" "^0.4.3" 456 | "@apollographql/graphql-playground-html" "1.6.24" 457 | "@types/graphql-upload" "^8.0.0" 458 | "@types/ws" "^7.0.0" 459 | apollo-cache-control "^0.11.0" 460 | apollo-datasource "^0.7.1" 461 | apollo-engine-reporting "^2.0.0" 462 | apollo-server-caching "^0.5.1" 463 | apollo-server-env "^2.4.4" 464 | apollo-server-errors "^2.4.1" 465 | apollo-server-plugin-base "^0.9.0" 466 | apollo-server-types "^0.5.0" 467 | apollo-tracing "^0.11.0" 468 | fast-json-stable-stringify "^2.0.0" 469 | graphql-extensions "^0.12.2" 470 | graphql-tag "^2.9.2" 471 | graphql-tools "^4.0.0" 472 | graphql-upload "^8.0.2" 473 | loglevel "^1.6.7" 474 | sha.js "^2.4.11" 475 | subscriptions-transport-ws "^0.9.11" 476 | ws "^6.0.0" 477 | 478 | apollo-server-env@^2.4.3, apollo-server-env@^2.4.4: 479 | version "2.4.4" 480 | resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.4.tgz#12d2d0896dcb184478cba066c7a683ab18689ca1" 481 | dependencies: 482 | node-fetch "^2.1.2" 483 | util.promisify "^1.0.0" 484 | 485 | apollo-server-errors@^2.4.1: 486 | version "2.4.1" 487 | resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.4.1.tgz#16ad49de6c9134bfb2b7dede9842e73bb239dbe2" 488 | 489 | apollo-server-micro@^2.9.8: 490 | version "2.9.12" 491 | resolved "https://registry.yarnpkg.com/apollo-server-micro/-/apollo-server-micro-2.9.12.tgz#6d0355875ea97adfa2495172e36dfee9fc1b8c45" 492 | dependencies: 493 | "@apollographql/graphql-playground-html" "1.6.24" 494 | accept "^3.0.2" 495 | apollo-server-core "^2.9.12" 496 | apollo-server-types "^0.2.8" 497 | micro "^9.3.2" 498 | 499 | apollo-server-plugin-base@^0.9.0: 500 | version "0.9.0" 501 | resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.9.0.tgz#777f720a1ee827a66b8c159073ca30645f8bc625" 502 | dependencies: 503 | apollo-server-types "^0.5.0" 504 | 505 | apollo-server-types@^0.2.8: 506 | version "0.2.8" 507 | resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.8.tgz#729208a8dd72831af3aa4f1eb584022ada146e6b" 508 | dependencies: 509 | apollo-engine-reporting-protobuf "^0.4.4" 510 | apollo-server-caching "^0.5.0" 511 | apollo-server-env "^2.4.3" 512 | 513 | apollo-server-types@^0.5.0: 514 | version "0.5.0" 515 | resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.5.0.tgz#51f39c5fa610ece8b07f1fbcf63c47d4ac150340" 516 | dependencies: 517 | apollo-engine-reporting-protobuf "^0.5.1" 518 | apollo-server-caching "^0.5.1" 519 | apollo-server-env "^2.4.4" 520 | 521 | apollo-tracing@^0.11.0: 522 | version "0.11.0" 523 | resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.11.0.tgz#8821eb60692f77c06660fb6bc147446f600aecfe" 524 | dependencies: 525 | apollo-server-env "^2.4.4" 526 | apollo-server-plugin-base "^0.9.0" 527 | 528 | apollo-utilities@^1.0.1, apollo-utilities@^1.3.0: 529 | version "1.3.4" 530 | resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.4.tgz#6129e438e8be201b6c55b0f13ce49d2c7175c9cf" 531 | dependencies: 532 | "@wry/equality" "^0.1.2" 533 | fast-json-stable-stringify "^2.0.0" 534 | ts-invariant "^0.4.0" 535 | tslib "^1.10.0" 536 | 537 | arg@4.1.0: 538 | version "4.1.0" 539 | resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" 540 | 541 | arrify@^2.0.0: 542 | version "2.0.1" 543 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" 544 | 545 | async-limiter@~1.0.0: 546 | version "1.0.1" 547 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" 548 | 549 | async-retry@^1.2.1: 550 | version "1.3.1" 551 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" 552 | dependencies: 553 | retry "0.12.0" 554 | 555 | asynckit@^0.4.0: 556 | version "0.4.0" 557 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 558 | 559 | backo2@^1.0.2: 560 | version "1.0.2" 561 | resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" 562 | 563 | base64-js@^1.3.0: 564 | version "1.3.1" 565 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" 566 | 567 | bignumber.js@^7.0.0: 568 | version "7.2.1" 569 | resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" 570 | 571 | bluebird@3.5.1: 572 | version "3.5.1" 573 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" 574 | 575 | boom@7.x.x: 576 | version "7.3.0" 577 | resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9" 578 | dependencies: 579 | hoek "6.x.x" 580 | 581 | bson@^1.1.1, bson@~1.1.1: 582 | version "1.1.3" 583 | resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" 584 | 585 | buffer-equal-constant-time@1.0.1: 586 | version "1.0.1" 587 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 588 | 589 | buffer-from@^1.0.0: 590 | version "1.1.1" 591 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 592 | 593 | bun@^0.0.12: 594 | version "0.0.12" 595 | resolved "https://registry.yarnpkg.com/bun/-/bun-0.0.12.tgz#d54fae69f895557f275423bc14b404030b20a5fc" 596 | dependencies: 597 | readable-stream "~1.0.32" 598 | 599 | busboy@^0.3.1: 600 | version "0.3.1" 601 | resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" 602 | dependencies: 603 | dicer "0.3.0" 604 | 605 | bytes@3.0.0: 606 | version "3.0.0" 607 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 608 | 609 | combined-stream@^1.0.8: 610 | version "1.0.8" 611 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 612 | dependencies: 613 | delayed-stream "~1.0.0" 614 | 615 | compressible@^2.0.12: 616 | version "2.0.17" 617 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" 618 | dependencies: 619 | mime-db ">= 1.40.0 < 2" 620 | 621 | concat-stream@^2.0.0: 622 | version "2.0.0" 623 | resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" 624 | dependencies: 625 | buffer-from "^1.0.0" 626 | inherits "^2.0.3" 627 | readable-stream "^3.0.2" 628 | typedarray "^0.0.6" 629 | 630 | configstore@^5.0.0: 631 | version "5.0.0" 632 | resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.0.tgz#37de662c7a49b5fe8dbcf8f6f5818d2d81ed852b" 633 | dependencies: 634 | dot-prop "^5.1.0" 635 | graceful-fs "^4.1.2" 636 | make-dir "^3.0.0" 637 | unique-string "^2.0.0" 638 | write-file-atomic "^3.0.0" 639 | xdg-basedir "^4.0.0" 640 | 641 | content-type@1.0.4: 642 | version "1.0.4" 643 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 644 | 645 | core-js@^3.0.1: 646 | version "3.6.5" 647 | resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" 648 | 649 | core-util-is@~1.0.0: 650 | version "1.0.2" 651 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 652 | 653 | crypto-random-string@^2.0.0: 654 | version "2.0.0" 655 | resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" 656 | 657 | date-and-time@^0.11.0: 658 | version "0.11.0" 659 | resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.11.0.tgz#1e22b61533af303953d79cc8c5e92e228fc5e4d2" 660 | 661 | debug@3.1.0: 662 | version "3.1.0" 663 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 664 | dependencies: 665 | ms "2.0.0" 666 | 667 | debug@^3.1.0: 668 | version "3.2.6" 669 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 670 | dependencies: 671 | ms "^2.1.1" 672 | 673 | debug@^4.1.1: 674 | version "4.1.1" 675 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 676 | dependencies: 677 | ms "^2.1.1" 678 | 679 | deep-equal@^1.0.1: 680 | version "1.1.1" 681 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" 682 | dependencies: 683 | is-arguments "^1.0.4" 684 | is-date-object "^1.0.1" 685 | is-regex "^1.0.4" 686 | object-is "^1.0.1" 687 | object-keys "^1.1.1" 688 | regexp.prototype.flags "^1.2.0" 689 | 690 | define-properties@^1.1.2, define-properties@^1.1.3: 691 | version "1.1.3" 692 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" 693 | dependencies: 694 | object-keys "^1.0.12" 695 | 696 | delayed-stream@~1.0.0: 697 | version "1.0.0" 698 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 699 | 700 | depd@1.1.1: 701 | version "1.1.1" 702 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 703 | 704 | depd@~1.1.2: 705 | version "1.1.2" 706 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 707 | 708 | deprecated-decorator@^0.1.6: 709 | version "0.1.6" 710 | resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" 711 | 712 | dicer@0.3.0, dicer@^0.3.0: 713 | version "0.3.0" 714 | resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" 715 | dependencies: 716 | streamsearch "0.1.2" 717 | 718 | dot-prop@^5.1.0: 719 | version "5.2.0" 720 | resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" 721 | dependencies: 722 | is-obj "^2.0.0" 723 | 724 | duplexify@^3.5.0, duplexify@^3.6.0: 725 | version "3.7.1" 726 | resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" 727 | dependencies: 728 | end-of-stream "^1.0.0" 729 | inherits "^2.0.1" 730 | readable-stream "^2.0.0" 731 | stream-shift "^1.0.0" 732 | 733 | duplexify@^4.1.1: 734 | version "4.1.1" 735 | resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" 736 | dependencies: 737 | end-of-stream "^1.4.1" 738 | inherits "^2.0.3" 739 | readable-stream "^3.1.1" 740 | stream-shift "^1.0.0" 741 | 742 | ecdsa-sig-formatter@1.0.11: 743 | version "1.0.11" 744 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 745 | dependencies: 746 | safe-buffer "^5.0.1" 747 | 748 | end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: 749 | version "1.4.4" 750 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 751 | dependencies: 752 | once "^1.4.0" 753 | 754 | ent@^2.2.0: 755 | version "2.2.0" 756 | resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" 757 | 758 | es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: 759 | version "1.17.5" 760 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" 761 | dependencies: 762 | es-to-primitive "^1.2.1" 763 | function-bind "^1.1.1" 764 | has "^1.0.3" 765 | has-symbols "^1.0.1" 766 | is-callable "^1.1.5" 767 | is-regex "^1.0.5" 768 | object-inspect "^1.7.0" 769 | object-keys "^1.1.1" 770 | object.assign "^4.1.0" 771 | string.prototype.trimleft "^2.1.1" 772 | string.prototype.trimright "^2.1.1" 773 | 774 | es-to-primitive@^1.2.1: 775 | version "1.2.1" 776 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" 777 | dependencies: 778 | is-callable "^1.1.4" 779 | is-date-object "^1.0.1" 780 | is-symbol "^1.0.2" 781 | 782 | es6-promise@^4.0.3: 783 | version "4.2.8" 784 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" 785 | 786 | es6-promisify@^5.0.0: 787 | version "5.0.0" 788 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 789 | dependencies: 790 | es6-promise "^4.0.3" 791 | 792 | event-target-shim@^5.0.0: 793 | version "5.0.1" 794 | resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 795 | 796 | eventemitter3@^3.1.0: 797 | version "3.1.2" 798 | resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" 799 | 800 | extend@^3.0.2: 801 | version "3.0.2" 802 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 803 | 804 | fast-json-stable-stringify@^2.0.0: 805 | version "2.1.0" 806 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 807 | 808 | fast-text-encoding@^1.0.0: 809 | version "1.0.0" 810 | resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef" 811 | 812 | faye-websocket@0.11.3: 813 | version "0.11.3" 814 | resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" 815 | dependencies: 816 | websocket-driver ">=0.5.1" 817 | 818 | firebase-admin@^8.7.0: 819 | version "8.8.0" 820 | resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-8.8.0.tgz#6fbe5a8353d13d61dee504a54e18b7ef82d0d8a8" 821 | dependencies: 822 | "@firebase/database" "^0.5.11" 823 | "@types/node" "^8.0.53" 824 | dicer "^0.3.0" 825 | jsonwebtoken "8.1.0" 826 | node-forge "0.7.4" 827 | optionalDependencies: 828 | "@google-cloud/firestore" "^2.6.0" 829 | "@google-cloud/storage" "^4.1.2" 830 | 831 | form-data@^3.0.0: 832 | version "3.0.0" 833 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" 834 | dependencies: 835 | asynckit "^0.4.0" 836 | combined-stream "^1.0.8" 837 | mime-types "^2.1.12" 838 | 839 | fs-capacitor@^2.0.4: 840 | version "2.0.4" 841 | resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" 842 | 843 | function-bind@^1.1.1: 844 | version "1.1.1" 845 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 846 | 847 | functional-red-black-tree@^1.0.1: 848 | version "1.0.1" 849 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 850 | 851 | gaxios@^2.0.0, gaxios@^2.0.1, gaxios@^2.1.0: 852 | version "2.1.0" 853 | resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.1.0.tgz#b5d04ec19bf853d4589ccc2e7d61f0f2ab62afee" 854 | dependencies: 855 | abort-controller "^3.0.0" 856 | extend "^3.0.2" 857 | https-proxy-agent "^3.0.0" 858 | is-stream "^2.0.0" 859 | node-fetch "^2.3.0" 860 | 861 | gcp-metadata@^3.2.0: 862 | version "3.2.2" 863 | resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.2.2.tgz#dcac6bf65775d5caa3a2e161469c0af068849256" 864 | dependencies: 865 | gaxios "^2.1.0" 866 | json-bigint "^0.3.0" 867 | 868 | gcs-resumable-upload@^2.2.4: 869 | version "2.3.1" 870 | resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-2.3.1.tgz#5fa7c3035108bba7a2f95435377ad92acdbae656" 871 | dependencies: 872 | abort-controller "^3.0.0" 873 | configstore "^5.0.0" 874 | gaxios "^2.0.0" 875 | google-auth-library "^5.0.0" 876 | pumpify "^2.0.0" 877 | stream-events "^1.0.4" 878 | 879 | google-auth-library@^5.0.0, google-auth-library@^5.5.0: 880 | version "5.5.1" 881 | resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-5.5.1.tgz#2bf5ade93cb9d00c860d3fb15798db33b39a53a7" 882 | dependencies: 883 | arrify "^2.0.0" 884 | base64-js "^1.3.0" 885 | fast-text-encoding "^1.0.0" 886 | gaxios "^2.1.0" 887 | gcp-metadata "^3.2.0" 888 | gtoken "^4.1.0" 889 | jws "^3.1.5" 890 | lru-cache "^5.0.0" 891 | 892 | google-gax@^1.7.5: 893 | version "1.11.1" 894 | resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-1.11.1.tgz#9be67c762f25445a6d8d410207f2c6e180b3b947" 895 | dependencies: 896 | "@grpc/grpc-js" "0.6.9" 897 | "@grpc/proto-loader" "^0.5.1" 898 | "@types/long" "^4.0.0" 899 | abort-controller "^3.0.0" 900 | duplexify "^3.6.0" 901 | google-auth-library "^5.0.0" 902 | is-stream-ended "^0.1.4" 903 | lodash.at "^4.6.0" 904 | lodash.has "^4.5.2" 905 | node-fetch "^2.6.0" 906 | protobufjs "^6.8.8" 907 | retry-request "^4.0.0" 908 | semver "^6.0.0" 909 | walkdir "^0.4.0" 910 | 911 | google-p12-pem@^2.0.0: 912 | version "2.0.3" 913 | resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.3.tgz#14ecd78a94bd03bf86d74d9d0724787e85c7731f" 914 | dependencies: 915 | node-forge "^0.9.0" 916 | 917 | graceful-fs@^4.1.2: 918 | version "4.2.3" 919 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" 920 | 921 | graphql-extensions@^0.12.2: 922 | version "0.12.2" 923 | resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.12.2.tgz#f22210e812939b7caa2127589f30e6a1c671540f" 924 | dependencies: 925 | "@apollographql/apollo-tools" "^0.4.3" 926 | apollo-server-env "^2.4.4" 927 | apollo-server-types "^0.5.0" 928 | 929 | graphql-tag@^2.9.2: 930 | version "2.10.3" 931 | resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" 932 | 933 | graphql-tools@^4.0.0: 934 | version "4.0.8" 935 | resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.8.tgz#e7fb9f0d43408fb0878ba66b522ce871bafe9d30" 936 | dependencies: 937 | apollo-link "^1.2.14" 938 | apollo-utilities "^1.0.1" 939 | deprecated-decorator "^0.1.6" 940 | iterall "^1.1.3" 941 | uuid "^3.1.0" 942 | 943 | graphql-upload@^8.0.2: 944 | version "8.1.0" 945 | resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.1.0.tgz#6d0ab662db5677a68bfb1f2c870ab2544c14939a" 946 | dependencies: 947 | busboy "^0.3.1" 948 | fs-capacitor "^2.0.4" 949 | http-errors "^1.7.3" 950 | object-path "^0.11.4" 951 | 952 | graphql@^14.5.3: 953 | version "14.6.0" 954 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" 955 | dependencies: 956 | iterall "^1.2.2" 957 | 958 | graphql@^14.5.8: 959 | version "14.5.8" 960 | resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.5.8.tgz#504f3d3114cb9a0a3f359bbbcf38d9e5bf6a6b3c" 961 | dependencies: 962 | iterall "^1.2.2" 963 | 964 | gtoken@^4.1.0: 965 | version "4.1.3" 966 | resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-4.1.3.tgz#efa9e42f59d02731f15de466b09331d7afc393cf" 967 | dependencies: 968 | gaxios "^2.1.0" 969 | google-p12-pem "^2.0.0" 970 | jws "^3.1.5" 971 | mime "^2.2.0" 972 | 973 | has-symbols@^1.0.0, has-symbols@^1.0.1: 974 | version "1.0.1" 975 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" 976 | 977 | has@^1.0.1, has@^1.0.3: 978 | version "1.0.3" 979 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 980 | dependencies: 981 | function-bind "^1.1.1" 982 | 983 | hash-stream-validation@^0.2.2: 984 | version "0.2.2" 985 | resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz#6b34c4fce5e9fce265f1d3380900049d92a10090" 986 | dependencies: 987 | through2 "^2.0.0" 988 | 989 | hoek@6.x.x: 990 | version "6.1.3" 991 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" 992 | 993 | http-errors@1.6.2: 994 | version "1.6.2" 995 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 996 | dependencies: 997 | depd "1.1.1" 998 | inherits "2.0.3" 999 | setprototypeof "1.0.3" 1000 | statuses ">= 1.3.1 < 2" 1001 | 1002 | http-errors@^1.7.3: 1003 | version "1.7.3" 1004 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" 1005 | dependencies: 1006 | depd "~1.1.2" 1007 | inherits "2.0.4" 1008 | setprototypeof "1.1.1" 1009 | statuses ">= 1.5.0 < 2" 1010 | toidentifier "1.0.0" 1011 | 1012 | "http-parser-js@>=0.4.0 <0.4.11": 1013 | version "0.4.10" 1014 | resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" 1015 | 1016 | http-proxy-agent@^2.1.0: 1017 | version "2.1.0" 1018 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" 1019 | dependencies: 1020 | agent-base "4" 1021 | debug "3.1.0" 1022 | 1023 | https-proxy-agent@^3.0.0: 1024 | version "3.0.1" 1025 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" 1026 | dependencies: 1027 | agent-base "^4.3.0" 1028 | debug "^3.1.0" 1029 | 1030 | iconv-lite@0.4.19: 1031 | version "0.4.19" 1032 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 1033 | 1034 | imurmurhash@^0.1.4: 1035 | version "0.1.4" 1036 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 1037 | 1038 | inherits@2.0.3: 1039 | version "2.0.3" 1040 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 1041 | 1042 | inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: 1043 | version "2.0.4" 1044 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 1045 | 1046 | is-arguments@^1.0.4: 1047 | version "1.0.4" 1048 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" 1049 | 1050 | is-callable@^1.1.4, is-callable@^1.1.5: 1051 | version "1.2.0" 1052 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" 1053 | 1054 | is-date-object@^1.0.1: 1055 | version "1.0.2" 1056 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" 1057 | 1058 | is-obj@^2.0.0: 1059 | version "2.0.0" 1060 | resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" 1061 | 1062 | is-regex@^1.0.4: 1063 | version "1.0.4" 1064 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" 1065 | dependencies: 1066 | has "^1.0.1" 1067 | 1068 | is-regex@^1.0.5: 1069 | version "1.1.0" 1070 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" 1071 | dependencies: 1072 | has-symbols "^1.0.1" 1073 | 1074 | is-stream-ended@^0.1.4: 1075 | version "0.1.4" 1076 | resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" 1077 | 1078 | is-stream@1.1.0: 1079 | version "1.1.0" 1080 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 1081 | 1082 | is-stream@^2.0.0: 1083 | version "2.0.0" 1084 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" 1085 | 1086 | is-symbol@^1.0.2: 1087 | version "1.0.3" 1088 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" 1089 | dependencies: 1090 | has-symbols "^1.0.1" 1091 | 1092 | is-typedarray@^1.0.0: 1093 | version "1.0.0" 1094 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 1095 | 1096 | isarray@0.0.1: 1097 | version "0.0.1" 1098 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 1099 | 1100 | isarray@~1.0.0: 1101 | version "1.0.0" 1102 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 1103 | 1104 | iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: 1105 | version "1.3.0" 1106 | resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" 1107 | 1108 | json-bigint@^0.3.0: 1109 | version "0.3.0" 1110 | resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.0.tgz#0ccd912c4b8270d05f056fbd13814b53d3825b1e" 1111 | dependencies: 1112 | bignumber.js "^7.0.0" 1113 | 1114 | jsonwebtoken@8.1.0: 1115 | version "8.1.0" 1116 | resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83" 1117 | dependencies: 1118 | jws "^3.1.4" 1119 | lodash.includes "^4.3.0" 1120 | lodash.isboolean "^3.0.3" 1121 | lodash.isinteger "^4.0.4" 1122 | lodash.isnumber "^3.0.3" 1123 | lodash.isplainobject "^4.0.6" 1124 | lodash.isstring "^4.0.1" 1125 | lodash.once "^4.0.0" 1126 | ms "^2.0.0" 1127 | xtend "^4.0.1" 1128 | 1129 | jwa@^1.4.1: 1130 | version "1.4.1" 1131 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 1132 | dependencies: 1133 | buffer-equal-constant-time "1.0.1" 1134 | ecdsa-sig-formatter "1.0.11" 1135 | safe-buffer "^5.0.1" 1136 | 1137 | jws@^3.1.4, jws@^3.1.5: 1138 | version "3.2.2" 1139 | resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" 1140 | dependencies: 1141 | jwa "^1.4.1" 1142 | safe-buffer "^5.0.1" 1143 | 1144 | kareem@2.3.1: 1145 | version "2.3.1" 1146 | resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" 1147 | 1148 | lodash.at@^4.6.0: 1149 | version "4.6.0" 1150 | resolved "https://registry.yarnpkg.com/lodash.at/-/lodash.at-4.6.0.tgz#93cdce664f0a1994ea33dd7cd40e23afd11b0ff8" 1151 | 1152 | lodash.camelcase@^4.3.0: 1153 | version "4.3.0" 1154 | resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" 1155 | 1156 | lodash.has@^4.5.2: 1157 | version "4.5.2" 1158 | resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" 1159 | 1160 | lodash.includes@^4.3.0: 1161 | version "4.3.0" 1162 | resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" 1163 | 1164 | lodash.isboolean@^3.0.3: 1165 | version "3.0.3" 1166 | resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" 1167 | 1168 | lodash.isinteger@^4.0.4: 1169 | version "4.0.4" 1170 | resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" 1171 | 1172 | lodash.isnumber@^3.0.3: 1173 | version "3.0.3" 1174 | resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" 1175 | 1176 | lodash.isplainobject@^4.0.6: 1177 | version "4.0.6" 1178 | resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" 1179 | 1180 | lodash.isstring@^4.0.1: 1181 | version "4.0.1" 1182 | resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" 1183 | 1184 | lodash.once@^4.0.0: 1185 | version "4.1.1" 1186 | resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" 1187 | 1188 | lodash.sortby@^4.7.0: 1189 | version "4.7.0" 1190 | resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" 1191 | 1192 | loglevel@^1.6.7: 1193 | version "1.6.8" 1194 | resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" 1195 | 1196 | long@^4.0.0: 1197 | version "4.0.0" 1198 | resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" 1199 | 1200 | lru-cache@^5.0.0: 1201 | version "5.1.1" 1202 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" 1203 | dependencies: 1204 | yallist "^3.0.2" 1205 | 1206 | make-dir@^3.0.0: 1207 | version "3.0.0" 1208 | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" 1209 | dependencies: 1210 | semver "^6.0.0" 1211 | 1212 | memory-pager@^1.0.2: 1213 | version "1.5.0" 1214 | resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" 1215 | 1216 | micro@^9.3.2: 1217 | version "9.3.4" 1218 | resolved "https://registry.yarnpkg.com/micro/-/micro-9.3.4.tgz#745a494e53c8916f64fb6a729f8cbf2a506b35ad" 1219 | dependencies: 1220 | arg "4.1.0" 1221 | content-type "1.0.4" 1222 | is-stream "1.1.0" 1223 | raw-body "2.3.2" 1224 | 1225 | mime-db@1.42.0, "mime-db@>= 1.40.0 < 2": 1226 | version "1.42.0" 1227 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" 1228 | 1229 | mime-db@1.44.0: 1230 | version "1.44.0" 1231 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 1232 | 1233 | mime-types@^2.0.8: 1234 | version "2.1.25" 1235 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" 1236 | dependencies: 1237 | mime-db "1.42.0" 1238 | 1239 | mime-types@^2.1.12: 1240 | version "2.1.27" 1241 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 1242 | dependencies: 1243 | mime-db "1.44.0" 1244 | 1245 | mime@^2.2.0: 1246 | version "2.4.4" 1247 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" 1248 | 1249 | mimic-fn@^2.1.0: 1250 | version "2.1.0" 1251 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" 1252 | 1253 | moment@*, moment@^2.24.0: 1254 | version "2.24.0" 1255 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" 1256 | 1257 | mongodb@3.3.5, mongodb@^3.3.4: 1258 | version "3.3.5" 1259 | resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.5.tgz#38d531013afede92b0dd282e3b9f3c08c9bdff3b" 1260 | dependencies: 1261 | bson "^1.1.1" 1262 | require_optional "^1.0.1" 1263 | safe-buffer "^5.1.2" 1264 | optionalDependencies: 1265 | saslprep "^1.0.0" 1266 | 1267 | mongoose-legacy-pluralize@1.0.2: 1268 | version "1.0.2" 1269 | resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" 1270 | 1271 | mongoose@^5.7.12: 1272 | version "5.7.13" 1273 | resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.7.13.tgz#d8ebbc15cfb19d014cf1fbd4b7af413d75952d46" 1274 | dependencies: 1275 | bson "~1.1.1" 1276 | kareem "2.3.1" 1277 | mongodb "3.3.5" 1278 | mongoose-legacy-pluralize "1.0.2" 1279 | mpath "0.6.0" 1280 | mquery "3.2.2" 1281 | ms "2.1.2" 1282 | regexp-clone "1.0.0" 1283 | safe-buffer "5.1.2" 1284 | sift "7.0.1" 1285 | sliced "1.0.1" 1286 | 1287 | mpath@0.6.0: 1288 | version "0.6.0" 1289 | resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" 1290 | 1291 | mquery@3.2.2: 1292 | version "3.2.2" 1293 | resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" 1294 | dependencies: 1295 | bluebird "3.5.1" 1296 | debug "3.1.0" 1297 | regexp-clone "^1.0.0" 1298 | safe-buffer "5.1.2" 1299 | sliced "1.0.1" 1300 | 1301 | ms@2.0.0: 1302 | version "2.0.0" 1303 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 1304 | 1305 | ms@2.1.2, ms@^2.0.0, ms@^2.1.1: 1306 | version "2.1.2" 1307 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 1308 | 1309 | node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0: 1310 | version "2.6.1" 1311 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" 1312 | 1313 | node-forge@0.7.4: 1314 | version "0.7.4" 1315 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.4.tgz#8e6e9f563a1e32213aa7508cded22aa791dbf986" 1316 | 1317 | node-forge@^0.9.0: 1318 | version "0.9.1" 1319 | resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" 1320 | 1321 | object-inspect@^1.7.0: 1322 | version "1.7.0" 1323 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" 1324 | 1325 | object-is@^1.0.1: 1326 | version "1.0.1" 1327 | resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6" 1328 | 1329 | object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: 1330 | version "1.1.1" 1331 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 1332 | 1333 | object-path@^0.11.4: 1334 | version "0.11.5" 1335 | resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.5.tgz#d4e3cf19601a5140a55a16ad712019a9c50b577a" 1336 | 1337 | object.assign@^4.1.0: 1338 | version "4.1.0" 1339 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" 1340 | dependencies: 1341 | define-properties "^1.1.2" 1342 | function-bind "^1.1.1" 1343 | has-symbols "^1.0.0" 1344 | object-keys "^1.0.11" 1345 | 1346 | object.getownpropertydescriptors@^2.1.0: 1347 | version "2.1.0" 1348 | resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" 1349 | dependencies: 1350 | define-properties "^1.1.3" 1351 | es-abstract "^1.17.0-next.1" 1352 | 1353 | once@^1.3.1, once@^1.4.0: 1354 | version "1.4.0" 1355 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 1356 | dependencies: 1357 | wrappy "1" 1358 | 1359 | onetime@^5.1.0: 1360 | version "5.1.0" 1361 | resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" 1362 | dependencies: 1363 | mimic-fn "^2.1.0" 1364 | 1365 | p-limit@^2.2.0: 1366 | version "2.2.1" 1367 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" 1368 | dependencies: 1369 | p-try "^2.0.0" 1370 | 1371 | p-try@^2.0.0: 1372 | version "2.2.0" 1373 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 1374 | 1375 | process-nextick-args@~2.0.0: 1376 | version "2.0.1" 1377 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" 1378 | 1379 | protobufjs@^6.8.6, protobufjs@^6.8.8: 1380 | version "6.8.8" 1381 | resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" 1382 | dependencies: 1383 | "@protobufjs/aspromise" "^1.1.2" 1384 | "@protobufjs/base64" "^1.1.2" 1385 | "@protobufjs/codegen" "^2.0.4" 1386 | "@protobufjs/eventemitter" "^1.1.0" 1387 | "@protobufjs/fetch" "^1.1.0" 1388 | "@protobufjs/float" "^1.0.2" 1389 | "@protobufjs/inquire" "^1.1.0" 1390 | "@protobufjs/path" "^1.1.2" 1391 | "@protobufjs/pool" "^1.1.0" 1392 | "@protobufjs/utf8" "^1.1.0" 1393 | "@types/long" "^4.0.0" 1394 | "@types/node" "^10.1.0" 1395 | long "^4.0.0" 1396 | 1397 | pump@^3.0.0: 1398 | version "3.0.0" 1399 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 1400 | dependencies: 1401 | end-of-stream "^1.1.0" 1402 | once "^1.3.1" 1403 | 1404 | pumpify@^2.0.0: 1405 | version "2.0.1" 1406 | resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" 1407 | dependencies: 1408 | duplexify "^4.1.1" 1409 | inherits "^2.0.3" 1410 | pump "^3.0.0" 1411 | 1412 | raw-body@2.3.2: 1413 | version "2.3.2" 1414 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 1415 | dependencies: 1416 | bytes "3.0.0" 1417 | http-errors "1.6.2" 1418 | iconv-lite "0.4.19" 1419 | unpipe "1.0.0" 1420 | 1421 | "readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0: 1422 | version "3.4.0" 1423 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" 1424 | dependencies: 1425 | inherits "^2.0.3" 1426 | string_decoder "^1.1.1" 1427 | util-deprecate "^1.0.1" 1428 | 1429 | readable-stream@^2.0.0, readable-stream@~2.3.6: 1430 | version "2.3.6" 1431 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 1432 | dependencies: 1433 | core-util-is "~1.0.0" 1434 | inherits "~2.0.3" 1435 | isarray "~1.0.0" 1436 | process-nextick-args "~2.0.0" 1437 | safe-buffer "~5.1.1" 1438 | string_decoder "~1.1.1" 1439 | util-deprecate "~1.0.1" 1440 | 1441 | readable-stream@~1.0.32: 1442 | version "1.0.34" 1443 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" 1444 | dependencies: 1445 | core-util-is "~1.0.0" 1446 | inherits "~2.0.1" 1447 | isarray "0.0.1" 1448 | string_decoder "~0.10.x" 1449 | 1450 | regexp-clone@1.0.0, regexp-clone@^1.0.0: 1451 | version "1.0.0" 1452 | resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" 1453 | 1454 | regexp.prototype.flags@^1.2.0: 1455 | version "1.2.0" 1456 | resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz#6b30724e306a27833eeb171b66ac8890ba37e41c" 1457 | dependencies: 1458 | define-properties "^1.1.2" 1459 | 1460 | require_optional@^1.0.1: 1461 | version "1.0.1" 1462 | resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" 1463 | dependencies: 1464 | resolve-from "^2.0.0" 1465 | semver "^5.1.0" 1466 | 1467 | resolve-from@^2.0.0: 1468 | version "2.0.0" 1469 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" 1470 | 1471 | retry-request@^4.0.0: 1472 | version "4.1.1" 1473 | resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.1.tgz#f676d0db0de7a6f122c048626ce7ce12101d2bd8" 1474 | dependencies: 1475 | debug "^4.1.1" 1476 | through2 "^3.0.1" 1477 | 1478 | retry@0.12.0: 1479 | version "0.12.0" 1480 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" 1481 | 1482 | safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: 1483 | version "5.1.2" 1484 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1485 | 1486 | safe-buffer@>=5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.2.0: 1487 | version "5.2.0" 1488 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 1489 | 1490 | safe-buffer@^5.0.1: 1491 | version "5.2.1" 1492 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 1493 | 1494 | saslprep@^1.0.0: 1495 | version "1.0.3" 1496 | resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" 1497 | dependencies: 1498 | sparse-bitfield "^3.0.3" 1499 | 1500 | semver@^5.1.0: 1501 | version "5.7.1" 1502 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 1503 | 1504 | semver@^6.0.0, semver@^6.2.0: 1505 | version "6.3.0" 1506 | resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" 1507 | 1508 | setprototypeof@1.0.3: 1509 | version "1.0.3" 1510 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 1511 | 1512 | setprototypeof@1.1.1: 1513 | version "1.1.1" 1514 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" 1515 | 1516 | sha.js@^2.4.11: 1517 | version "2.4.11" 1518 | resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" 1519 | dependencies: 1520 | inherits "^2.0.1" 1521 | safe-buffer "^5.0.1" 1522 | 1523 | sift@7.0.1: 1524 | version "7.0.1" 1525 | resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" 1526 | 1527 | signal-exit@^3.0.2: 1528 | version "3.0.2" 1529 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1530 | 1531 | sliced@1.0.1: 1532 | version "1.0.1" 1533 | resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" 1534 | 1535 | snakeize@^0.1.0: 1536 | version "0.1.0" 1537 | resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" 1538 | 1539 | sparse-bitfield@^3.0.3: 1540 | version "3.0.3" 1541 | resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" 1542 | dependencies: 1543 | memory-pager "^1.0.2" 1544 | 1545 | "statuses@>= 1.3.1 < 2", "statuses@>= 1.5.0 < 2": 1546 | version "1.5.0" 1547 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 1548 | 1549 | stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5: 1550 | version "1.0.5" 1551 | resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" 1552 | dependencies: 1553 | stubs "^3.0.0" 1554 | 1555 | stream-shift@^1.0.0: 1556 | version "1.0.0" 1557 | resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" 1558 | 1559 | streamsearch@0.1.2: 1560 | version "0.1.2" 1561 | resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" 1562 | 1563 | string.prototype.trimend@^1.0.0: 1564 | version "1.0.1" 1565 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" 1566 | dependencies: 1567 | define-properties "^1.1.3" 1568 | es-abstract "^1.17.5" 1569 | 1570 | string.prototype.trimleft@^2.1.1: 1571 | version "2.1.2" 1572 | resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz#4408aa2e5d6ddd0c9a80739b087fbc067c03b3cc" 1573 | dependencies: 1574 | define-properties "^1.1.3" 1575 | es-abstract "^1.17.5" 1576 | string.prototype.trimstart "^1.0.0" 1577 | 1578 | string.prototype.trimright@^2.1.1: 1579 | version "2.1.2" 1580 | resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz#c76f1cef30f21bbad8afeb8db1511496cfb0f2a3" 1581 | dependencies: 1582 | define-properties "^1.1.3" 1583 | es-abstract "^1.17.5" 1584 | string.prototype.trimend "^1.0.0" 1585 | 1586 | string.prototype.trimstart@^1.0.0: 1587 | version "1.0.1" 1588 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" 1589 | dependencies: 1590 | define-properties "^1.1.3" 1591 | es-abstract "^1.17.5" 1592 | 1593 | string_decoder@^1.1.1: 1594 | version "1.3.0" 1595 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 1596 | dependencies: 1597 | safe-buffer "~5.2.0" 1598 | 1599 | string_decoder@~0.10.x: 1600 | version "0.10.31" 1601 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" 1602 | 1603 | string_decoder@~1.1.1: 1604 | version "1.1.1" 1605 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" 1606 | dependencies: 1607 | safe-buffer "~5.1.0" 1608 | 1609 | stubs@^3.0.0: 1610 | version "3.0.0" 1611 | resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" 1612 | 1613 | subscriptions-transport-ws@^0.9.11: 1614 | version "0.9.16" 1615 | resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" 1616 | dependencies: 1617 | backo2 "^1.0.2" 1618 | eventemitter3 "^3.1.0" 1619 | iterall "^1.2.1" 1620 | symbol-observable "^1.0.4" 1621 | ws "^5.2.0" 1622 | 1623 | symbol-observable@^1.0.4: 1624 | version "1.2.0" 1625 | resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" 1626 | 1627 | teeny-request@^5.2.1: 1628 | version "5.3.1" 1629 | resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-5.3.1.tgz#3a2a9a479e2d884eba814dc9c0c82b7979e69b4d" 1630 | dependencies: 1631 | http-proxy-agent "^2.1.0" 1632 | https-proxy-agent "^3.0.0" 1633 | node-fetch "^2.2.0" 1634 | stream-events "^1.0.5" 1635 | uuid "^3.3.2" 1636 | 1637 | through2@^2.0.0: 1638 | version "2.0.5" 1639 | resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" 1640 | dependencies: 1641 | readable-stream "~2.3.6" 1642 | xtend "~4.0.1" 1643 | 1644 | through2@^3.0.0, through2@^3.0.1: 1645 | version "3.0.1" 1646 | resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" 1647 | dependencies: 1648 | readable-stream "2 || 3" 1649 | 1650 | toidentifier@1.0.0: 1651 | version "1.0.0" 1652 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 1653 | 1654 | ts-invariant@^0.4.0: 1655 | version "0.4.4" 1656 | resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" 1657 | dependencies: 1658 | tslib "^1.9.3" 1659 | 1660 | tslib@1.10.0: 1661 | version "1.10.0" 1662 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" 1663 | 1664 | tslib@^1.10.0, tslib@^1.9.3: 1665 | version "1.13.0" 1666 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" 1667 | 1668 | typedarray-to-buffer@^3.1.5: 1669 | version "3.1.5" 1670 | resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" 1671 | dependencies: 1672 | is-typedarray "^1.0.0" 1673 | 1674 | typedarray@^0.0.6: 1675 | version "0.0.6" 1676 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 1677 | 1678 | typescript@^3.7.2: 1679 | version "3.7.2" 1680 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb" 1681 | 1682 | unique-string@^2.0.0: 1683 | version "2.0.0" 1684 | resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" 1685 | dependencies: 1686 | crypto-random-string "^2.0.0" 1687 | 1688 | unpipe@1.0.0: 1689 | version "1.0.0" 1690 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1691 | 1692 | util-deprecate@^1.0.1, util-deprecate@~1.0.1: 1693 | version "1.0.2" 1694 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1695 | 1696 | util.promisify@^1.0.0: 1697 | version "1.0.1" 1698 | resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" 1699 | dependencies: 1700 | define-properties "^1.1.3" 1701 | es-abstract "^1.17.2" 1702 | has-symbols "^1.0.1" 1703 | object.getownpropertydescriptors "^2.1.0" 1704 | 1705 | uuid@^3.1.0: 1706 | version "3.4.0" 1707 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" 1708 | 1709 | uuid@^3.3.2: 1710 | version "3.3.3" 1711 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" 1712 | 1713 | uuid@^8.0.0: 1714 | version "8.1.0" 1715 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" 1716 | 1717 | walkdir@^0.4.0: 1718 | version "0.4.1" 1719 | resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" 1720 | 1721 | websocket-driver@>=0.5.1: 1722 | version "0.7.3" 1723 | resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" 1724 | dependencies: 1725 | http-parser-js ">=0.4.0 <0.4.11" 1726 | safe-buffer ">=5.1.0" 1727 | websocket-extensions ">=0.1.1" 1728 | 1729 | websocket-extensions@>=0.1.1: 1730 | version "0.1.4" 1731 | resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" 1732 | 1733 | wrappy@1: 1734 | version "1.0.2" 1735 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1736 | 1737 | write-file-atomic@^3.0.0: 1738 | version "3.0.1" 1739 | resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.1.tgz#558328352e673b5bb192cf86500d60b230667d4b" 1740 | dependencies: 1741 | imurmurhash "^0.1.4" 1742 | is-typedarray "^1.0.0" 1743 | signal-exit "^3.0.2" 1744 | typedarray-to-buffer "^3.1.5" 1745 | 1746 | ws@^5.2.0: 1747 | version "5.2.2" 1748 | resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" 1749 | dependencies: 1750 | async-limiter "~1.0.0" 1751 | 1752 | ws@^6.0.0: 1753 | version "6.2.1" 1754 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" 1755 | dependencies: 1756 | async-limiter "~1.0.0" 1757 | 1758 | xdg-basedir@^4.0.0: 1759 | version "4.0.0" 1760 | resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" 1761 | 1762 | xtend@^4.0.1, xtend@~4.0.1: 1763 | version "4.0.2" 1764 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 1765 | 1766 | yallist@^3.0.2: 1767 | version "3.1.1" 1768 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" 1769 | 1770 | zen-observable-ts@^0.8.21: 1771 | version "0.8.21" 1772 | resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz#85d0031fbbde1eba3cd07d3ba90da241215f421d" 1773 | dependencies: 1774 | tslib "^1.9.3" 1775 | zen-observable "^0.8.0" 1776 | 1777 | zen-observable@^0.8.0: 1778 | version "0.8.15" 1779 | resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" 1780 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /webapp/.npmrc: -------------------------------------------------------------------------------- 1 | @fortawesome:registry=https://npm.fontawesome.com/ 2 | //npm.fontawesome.com/:_authToken=${FONTAWESOME_NPM_AUTH_TOKEN} -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/react-hooks": "^3.1.3", 7 | "@emotion/core": "^10.0.22", 8 | "@emotion/styled": "^10.0.23", 9 | "@fortawesome/fontawesome-svg-core": "^1.2.25", 10 | "@fortawesome/free-brands-svg-icons": "^5.11.2", 11 | "@fortawesome/pro-light-svg-icons": "^5.11.2", 12 | "@fortawesome/pro-regular-svg-icons": "^5.11.2", 13 | "@fortawesome/pro-solid-svg-icons": "^5.11.2", 14 | "@fortawesome/react-fontawesome": "^0.1.7", 15 | "animejs": "^3.1.0", 16 | "apollo-boost": "^0.4.4", 17 | "graphql": "^14.5.8", 18 | "lodash.debounce": "^4.0.8", 19 | "moment": "^2.24.0", 20 | "react": "^16.12.0", 21 | "react-dom": "^16.12.0", 22 | "react-hooks-use-previous": "^1.0.0", 23 | "react-loading-skeleton": "^1.3.0", 24 | "react-pose": "^4.0.10", 25 | "react-router-dom": "^5.1.2", 26 | "react-scripts": "3.2.0", 27 | "react-slick": "^0.25.2", 28 | "typeface-roboto": "^0.0.75" 29 | }, 30 | "scripts": { 31 | "now-dev": "BROWSER=none SKIP_PREFLIGHT_CHECK=true react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "@types/animejs": "^3.1.0", 50 | "@types/lodash.debounce": "^4.0.6", 51 | "@types/react": "16.9.13", 52 | "@types/react-dom": "16.9.4", 53 | "@types/react-router-dom": "^5.1.3", 54 | "@types/react-slick": "^0.23.4", 55 | "react-hot-loader": "^4.12.18", 56 | "typescript": "3.7.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Work Commute 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /webapp/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/webapp/public/logo192.png -------------------------------------------------------------------------------- /webapp/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfviolato/work-commute/c5a062ec4b56746f70562eb4bb3f3f98cdc11bb3/webapp/public/logo512.png -------------------------------------------------------------------------------- /webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#323232", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /webapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /webapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hot } from 'react-hot-loader/root'; 3 | import ApolloClient from 'apollo-boost'; 4 | import styled from '@emotion/styled'; 5 | import { ApolloProvider } from '@apollo/react-hooks'; 6 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 7 | import { SkeletonTheme } from 'react-loading-skeleton'; 8 | import { Section } from './components/Section'; 9 | import { Period } from './components/Period'; 10 | import { Today } from './components/Today'; 11 | import { Navigation } from './components/Navigation'; 12 | 13 | import { GlobalStyles } from './global-styles'; 14 | 15 | const client = new ApolloClient({ uri: '/gql' }); 16 | 17 | const Content = styled.section` 18 | width: 100%; 19 | max-width: 1180px; 20 | margin: 0 auto; 21 | padding: 40px 4% 60px; 22 | 23 | @media (max-width: 375px) { 24 | padding-left: 8%; 25 | padding-right: 8%; 26 | } 27 | `; 28 | 29 | const App: React.FC = () => { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | 40 |
41 |
42 | 43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 | ); 55 | }; 56 | 57 | export default process.env.NODE_ENV === 'development' ? hot(App) : App; 58 | -------------------------------------------------------------------------------- /webapp/src/components/Averages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { faBriefcase, faTrain } from '@fortawesome/pro-solid-svg-icons'; 3 | import { TimeDisplay } from '../TimeDisplay'; 4 | import { IconLabel } from '../IconLabel'; 5 | import { useQuery } from '@apollo/react-hooks'; 6 | import query from './query'; 7 | import { 8 | IAveragesQueryData, 9 | IAveragesComponentProps, 10 | IAveragesProps, 11 | } from './interface'; 12 | import { Root } from './styled'; 13 | 14 | export const Averages: React.FC = ({ 15 | periodStart, 16 | periodEnd, 17 | }) => { 18 | const { loading, data, error } = useQuery(query, { 19 | variables: { 20 | periodStart, 21 | periodEnd, 22 | }, 23 | }); 24 | 25 | if (data && data.Period) { 26 | const { 27 | Period: { averageTimeCommuting, averageTimeAtOffice }, 28 | } = data; 29 | 30 | return ( 31 | 35 | ); 36 | } 37 | 38 | return ; 39 | }; 40 | 41 | const DEFAULT_TIME_PROP = { hours: 0, minutes: 0 }; 42 | 43 | export const AveragesComponent: React.FC = ({ 44 | averageTimeCommuting = DEFAULT_TIME_PROP, 45 | averageTimeAtOffice = DEFAULT_TIME_PROP, 46 | isLoading, 47 | hasError, 48 | }) => { 49 | return ( 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /webapp/src/components/Averages/interface.ts: -------------------------------------------------------------------------------- 1 | import { ITime, IPeriodResult } from '../../interfaces'; 2 | 3 | type PeriodQueryData = Pick< 4 | IPeriodResult, 5 | 'averageTimeAtOffice' | 'averageTimeCommuting' 6 | >; 7 | 8 | export interface IAveragesQueryData { 9 | Period: PeriodQueryData; 10 | } 11 | 12 | export interface IAveragesProps { 13 | periodStart: string; 14 | periodEnd: string; 15 | } 16 | 17 | export interface IAveragesComponentProps { 18 | averageTimeAtOffice?: ITime; 19 | averageTimeCommuting?: ITime; 20 | isLoading?: boolean; 21 | hasError?: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /webapp/src/components/Averages/query.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-boost'; 2 | 3 | export default gql` 4 | query getAveragesFromPeriod($periodStart: String!, $periodEnd: String!) { 5 | Period(periodStart: $periodStart, periodEnd: $periodEnd) { 6 | averageTimeAtOffice { 7 | hours 8 | minutes 9 | } 10 | 11 | averageTimeCommuting { 12 | hours 13 | minutes 14 | } 15 | } 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /webapp/src/components/Averages/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const CSS_VARS = { 4 | GRID_GUTTER: 32, 5 | GRID_COL_SIZE: 200, 6 | }; 7 | 8 | export const Root = styled.div` 9 | display: grid; 10 | grid-template-columns: repeat(2, ${CSS_VARS.GRID_COL_SIZE}px); 11 | grid-column-gap: ${CSS_VARS.GRID_GUTTER}px; 12 | 13 | @media (max-width: 520px) { 14 | grid-template-columns: ${CSS_VARS.GRID_COL_SIZE}px; 15 | grid-column-gap: 0; 16 | grid-row-gap: ${CSS_VARS.GRID_GUTTER}px; 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /webapp/src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ICardProps } from './interface'; 3 | import { Root } from './styled'; 4 | 5 | export const Card: React.FC = ({ children, className }) => { 6 | return {children}; 7 | }; 8 | -------------------------------------------------------------------------------- /webapp/src/components/Card/interface.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export interface ICardProps { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | -------------------------------------------------------------------------------- /webapp/src/components/Card/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { COLORS } from '../../global-styles'; 3 | 4 | export const Root = styled.div` 5 | position: relative; 6 | width: 100%; 7 | height: 100%; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | padding: 30px; 13 | border: 1px solid ${COLORS.GRAY}; 14 | border-radius: 4px; 15 | background-color: ${COLORS.LIGHT_BLACK}; 16 | `; 17 | -------------------------------------------------------------------------------- /webapp/src/components/DayTimetable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { faClock } from '@fortawesome/pro-regular-svg-icons'; 4 | import { useQuery } from '@apollo/react-hooks'; 5 | import { TimetableDisplay } from '../TimetableDisplay'; 6 | import { DEVELOPMENT_DAY, IS_DEV, DATE_FORMAT } from '../../constants'; 7 | import query from './query'; 8 | import { IDayTimetableQuery } from './interface'; 9 | 10 | const LABELS = { 11 | HOME_LEAVE: 'Home leave time', 12 | HOME_ARRIVE: 'Home arrive time', 13 | WORK_LEAVE: 'Work leave time', 14 | WORK_ARRIVE: 'Work arrive time', 15 | }; 16 | 17 | export const DayTimetable: React.FC = () => { 18 | const day = IS_DEV ? DEVELOPMENT_DAY : moment().format(DATE_FORMAT); 19 | const { loading, data, error } = useQuery(query, { 20 | variables: { day }, 21 | }); 22 | 23 | if (data && data.Day) { 24 | const { 25 | Day: { homeLeaveTime, homeArriveTime, workArriveTime, workLeaveTime }, 26 | } = data; 27 | 28 | return ( 29 | 38 | ); 39 | } 40 | 41 | return ( 42 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /webapp/src/components/DayTimetable/interface.ts: -------------------------------------------------------------------------------- 1 | import { IDayResult } from '../../interfaces'; 2 | 3 | type DayTimetableQueryData = Pick< 4 | IDayResult, 5 | 'homeArriveTime' | 'homeLeaveTime' | 'workArriveTime' | 'workLeaveTime' 6 | >; 7 | export interface IDayTimetableQuery { 8 | Day: DayTimetableQueryData; 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/components/DayTimetable/query.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-boost'; 2 | 3 | export default gql` 4 | query getDayTimetable($day: String!) { 5 | Day(day: $day) { 6 | homeLeaveTime 7 | workArriveTime 8 | workLeaveTime 9 | homeArriveTime 10 | } 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /webapp/src/components/DayTotal/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { useQuery } from '@apollo/react-hooks'; 4 | import { 5 | faBuilding, 6 | faSunHaze, 7 | faCloudsMoon, 8 | } from '@fortawesome/pro-solid-svg-icons'; 9 | import query from './query'; 10 | import { IS_DEV, DEVELOPMENT_DAY, DATE_FORMAT } from '../../constants'; 11 | import { IconLabel } from '../IconLabel'; 12 | import { TimeDisplay } from '../TimeDisplay'; 13 | import { IDayTotalQuery } from './interface'; 14 | import { Root } from './styled'; 15 | 16 | const LABELS = { 17 | MORNING_COMMUTE: 'Morning commute', 18 | TIME_AT_THE_OFFICE: 'Time at the office', 19 | TOTAL_EVENING_COMMUTE: 'Evening commute', 20 | }; 21 | 22 | export const DayTotal: React.FC = () => { 23 | const day = IS_DEV ? DEVELOPMENT_DAY : moment().format(DATE_FORMAT); 24 | const { data, loading, error } = useQuery(query, { 25 | variables: { day }, 26 | }); 27 | 28 | if (data && data.Day) { 29 | const { 30 | Day: { 31 | totalEveningCommuteTime, 32 | totalTimeAtOffice, 33 | totalMorningCommuteTime, 34 | }, 35 | } = data; 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | return ( 55 | 56 | 61 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /webapp/src/components/DayTotal/interface.ts: -------------------------------------------------------------------------------- 1 | import { IDayResult } from '../../interfaces'; 2 | 3 | type DayTimetableQueryData = Pick< 4 | IDayResult, 5 | 'totalMorningCommuteTime' | 'totalEveningCommuteTime' | 'totalTimeAtOffice' 6 | >; 7 | 8 | export interface IDayTotalQuery { 9 | Day: DayTimetableQueryData; 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/components/DayTotal/query.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-boost'; 2 | 3 | export default gql` 4 | query getDayTotals($day: String!) { 5 | Day(day: $day) { 6 | totalMorningCommuteTime { 7 | hours 8 | minutes 9 | } 10 | 11 | totalEveningCommuteTime { 12 | hours 13 | minutes 14 | } 15 | 16 | totalTimeAtOffice { 17 | hours 18 | minutes 19 | } 20 | } 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /webapp/src/components/DayTotal/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const CSS_VARS = { 4 | GRID_GUTTER: 32, 5 | MQ: { 6 | SMALL: 620, 7 | }, 8 | }; 9 | 10 | export const Root = styled.div` 11 | display: grid; 12 | width: 100%; 13 | grid-template-columns: repeat(3, 1fr); 14 | grid-column-gap: ${CSS_VARS.GRID_GUTTER}px; 15 | justify-content: center; 16 | 17 | @media (max-width: ${CSS_VARS.MQ.SMALL}px) { 18 | grid-template-columns: 100%; 19 | grid-column-gap: 0; 20 | grid-row-gap: 45px; 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /webapp/src/components/IconLabel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IIconLabelProps } from './interface'; 3 | import { 4 | Root, 5 | IconContainer, 6 | ErrorDisplay, 7 | Icon, 8 | Label, 9 | Content, 10 | } from './styled'; 11 | 12 | export const IconLabel: React.FC = ({ 13 | children, 14 | icon, 15 | label, 16 | hasError, 17 | }) => { 18 | return ( 19 | 20 | 21 | {hasError && } 22 | 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /webapp/src/components/IconLabel/interface.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 3 | 4 | export interface IIconLabelProps { 5 | children: ReactNode; 6 | label: string; 7 | icon: IconProp; 8 | hasError?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/components/IconLabel/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { QueryErrorIcon } from '../QueryErrorIcon'; 4 | import { COLORS } from '../../global-styles'; 5 | 6 | export const Root = styled.div` 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | flex-direction: column; 11 | `; 12 | 13 | export const IconContainer = styled.div` 14 | position: relative; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | width: 64px; 19 | height: 64px; 20 | border: 2px solid ${COLORS.GRAY}; 21 | border-radius: 50%; 22 | `; 23 | 24 | export const Icon = styled(FontAwesomeIcon)` 25 | font-size: 25px; 26 | position: relative; 27 | top: -1px; 28 | opacity: 0.9; 29 | `; 30 | 31 | export const Label = styled.div` 32 | font-size: 14px; 33 | margin-top: 10px; 34 | opacity: 0.9; 35 | `; 36 | 37 | export const Content = styled.div` 38 | display: flex; 39 | justify-content: center; 40 | border-top: 1px solid #aaa; 41 | width: 100%; 42 | text-align: center; 43 | margin-top: 20px; 44 | padding-top: 18px; 45 | line-height: 1; 46 | `; 47 | 48 | export const ErrorDisplay = styled(QueryErrorIcon)` 49 | position: absolute; 50 | top: -12px; 51 | right: -12px; 52 | `; 53 | -------------------------------------------------------------------------------- /webapp/src/components/ListItemPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IListItemPickerProps } from './interface'; 3 | import { 4 | faArrowCircleLeft, 5 | faArrowCircleRight, 6 | } from '@fortawesome/pro-regular-svg-icons'; 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 8 | import { NavigationButton, CurrentItemDisplay } from './styled'; 9 | 10 | export const ListItemPicker: React.FC = ({ 11 | list, 12 | index, 13 | onChange = () => {}, 14 | }) => { 15 | const [currentIndex, setCurrentIndex] = React.useState(index); 16 | const onPreviousClick = React.useCallback(() => { 17 | if (currentIndex > 0) { 18 | const newIndex = currentIndex - 1; 19 | 20 | setCurrentIndex(newIndex); 21 | onChange(list[newIndex], newIndex); 22 | } 23 | }, [currentIndex, onChange, list]); 24 | const onNextClick = React.useCallback(() => { 25 | if (currentIndex < list.length - 1) { 26 | const newIndex = currentIndex + 1; 27 | 28 | setCurrentIndex(newIndex); 29 | onChange(list[newIndex], newIndex); 30 | } 31 | }, [currentIndex, onChange, list]); 32 | 33 | React.useEffect(() => { 34 | setCurrentIndex(index); 35 | }, [index]); 36 | 37 | return ( 38 |
39 | 43 | 44 | 45 | 46 | {list[currentIndex]} 47 | 48 | 52 | 53 | 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /webapp/src/components/ListItemPicker/interface.ts: -------------------------------------------------------------------------------- 1 | export interface IListItemPickerProps { 2 | list: string[]; 3 | index: number; 4 | onChange?: (currentValue: string, currentIndex: number) => void; 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/components/ListItemPicker/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export interface INavigationButtonProps { 4 | isUnavailable: boolean; 5 | } 6 | 7 | export const NavigationButton = styled.button` 8 | font-size: 16px; 9 | transition: opacity 300ms ease; 10 | cursor: ${({ isUnavailable }) => (isUnavailable ? 'default' : 'pointer')}; 11 | opacity: ${({ isUnavailable }) => (isUnavailable ? 0.4 : 1)}; 12 | 13 | &:focus { 14 | outline: 0; 15 | } 16 | 17 | &:focus, 18 | &:hover { 19 | ${({ isUnavailable }) => !isUnavailable && { opacity: 0.8 }} 20 | } 21 | `; 22 | 23 | export interface INavigationButtonProps { 24 | isUnavailable: boolean; 25 | } 26 | 27 | export const CurrentItemDisplay = styled.span` 28 | margin: 0 10px; 29 | `; 30 | -------------------------------------------------------------------------------- /webapp/src/components/LoadingSpinner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { faCog } from '@fortawesome/pro-solid-svg-icons'; 3 | import { Spinner } from './styled'; 4 | import { ILoadingSpinnerProps } from './interface'; 5 | 6 | export const LoadingSpinner: React.FC = ({ className }) => { 7 | return ; 8 | }; 9 | -------------------------------------------------------------------------------- /webapp/src/components/LoadingSpinner/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ILoadingSpinnerProps { 2 | className?: string 3 | } -------------------------------------------------------------------------------- /webapp/src/components/LoadingSpinner/styled.tsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | import { keyframes } from '@emotion/core'; 3 | import styled from '@emotion/styled'; 4 | 5 | const rotate = keyframes` 6 | 50% { 7 | transform: rotate(180deg) scale(1.25); 8 | opacity: 1; 9 | } 10 | 11 | 100% { 12 | transform: rotate(360deg) scale(1); 13 | } 14 | `; 15 | 16 | export const Spinner = styled(FontAwesomeIcon)` 17 | opacity: 0.75; 18 | animation: ${rotate} 1500ms cubic-bezier(0.645, 0.045, 0.355, 1) infinite; 19 | `; 20 | -------------------------------------------------------------------------------- /webapp/src/components/MonthPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { PoseGroup } from 'react-pose'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faTimes } from '@fortawesome/pro-solid-svg-icons'; 6 | import { faCalendarAlt } from '@fortawesome/pro-regular-svg-icons'; 7 | import { useQuery } from '@apollo/react-hooks'; 8 | import Skeleton from 'react-loading-skeleton'; 9 | import { 10 | IMonthPickerProps, 11 | ICalendarMonth, 12 | IMonthPickerValue, 13 | IMonthPickerComponentProps, 14 | IMonthPickerQuery, 15 | } from './interface'; 16 | import { ListItemPicker } from '../ListItemPicker'; 17 | import query from './query'; 18 | import { 19 | Root, 20 | ScaledBg, 21 | RetractedTriggerBtn, 22 | RetractedTriggerBtnText, 23 | ErrorDisplay, 24 | Picker, 25 | ExpandedTriggerBtn, 26 | PickerYearContainer, 27 | PickerMonthContainer, 28 | PickerMonth, 29 | POSE_NAMES, 30 | } from './styled'; 31 | import { MONTH_DATE_FORMAT } from '../../constants'; 32 | import { useCalendarData } from './use-calendar-data'; 33 | 34 | const today = moment(); 35 | 36 | export const MonthPicker: React.FC = (props) => { 37 | const { loading, data, error } = useQuery(query); 38 | 39 | if (data && data.FirstRecord) { 40 | const { 41 | FirstRecord: { day }, 42 | } = data; 43 | 44 | const firstRecordDayDate = moment(day); 45 | 46 | return ( 47 | 52 | ); 53 | } 54 | 55 | return ( 56 | 57 | ); 58 | }; 59 | 60 | export const MonthPickerComponent: React.FC = ({ 61 | minYear = today.format('YYYY'), 62 | maxYear = today.format('MM'), 63 | isLoading = false, 64 | hasError = false, 65 | minMonth, 66 | maxMonth, 67 | currentYear, 68 | currentMonth, 69 | onSwitch = () => { }, 70 | }) => { 71 | const [isOpen, setIsOpen] = React.useState(false); 72 | const [isExpanded, setIsExpanded] = React.useState(false); 73 | const [browsingYear, setBrowsingYear] = React.useState(currentYear); 74 | const [currentValue, setCurrentValue] = React.useState({ 75 | year: currentYear, 76 | month: currentMonth, 77 | }); 78 | const { availableYearList, calendarMonthLabels } = useCalendarData({ 79 | isLoading, 80 | minYear, 81 | maxYear, 82 | minMonth, 83 | maxMonth, 84 | }); 85 | 86 | const onYearChange = React.useCallback( 87 | (year: string) => setBrowsingYear(year), 88 | [], 89 | ); 90 | 91 | const onComponentLeave = React.useCallback(() => { 92 | if (isOpen) { 93 | setIsOpen(false); 94 | } 95 | }, [isOpen]); 96 | 97 | React.useEffect( 98 | () => setCurrentValue({ month: currentMonth, year: currentYear }), 99 | [currentMonth, currentYear], 100 | ); 101 | 102 | React.useEffect(() => { 103 | if (availableYearList.length) { 104 | const availableYearIndex = availableYearList.indexOf(currentYear); 105 | const isYearUnavailable = availableYearIndex < 0; 106 | 107 | if (isYearUnavailable) { 108 | const latestAvailableYearIndex = availableYearList.length - 1; 109 | 110 | setBrowsingYear(availableYearList[latestAvailableYearIndex]); 111 | } 112 | } 113 | }, [availableYearList, currentYear]); 114 | 115 | return ( 116 | 117 | 124 | 125 | 126 | {!isOpen && ( 127 | !isLoading && setIsOpen(true)} 130 | onPoseComplete={(pose: string) => setIsExpanded(pose === 'exit')} 131 | > 132 | 133 | {isLoading ? ( 134 | 135 | ) : ( 136 | moment( 137 | `${currentValue.year}-${currentValue.month}`, 138 | MONTH_DATE_FORMAT, 139 | ).format('MMMM YYYY') 140 | )} 141 | 142 | 143 | {hasError ? ( 144 | 145 | ) : ( 146 | 147 | )} 148 | 149 | )} 150 | 151 | {isOpen && ( 152 | setIsExpanded(pose !== 'exit')} 155 | > 156 | setIsOpen(false)}> 157 | 158 | 159 | 160 | 161 | 166 | 167 | 168 | 169 | {calendarMonthLabels[browsingYear].map( 170 | ({ text, month, year, isAvailable }: ICalendarMonth) => { 171 | const isCurrent = 172 | currentValue.month === month && currentValue.year === year; 173 | 174 | const onClick = () => { 175 | if (isAvailable) { 176 | const newValue = { year: browsingYear, month }; 177 | 178 | setIsOpen(false); 179 | setCurrentValue(newValue); 180 | onSwitch(newValue); 181 | } 182 | }; 183 | 184 | return ( 185 | 191 | {text} 192 | 193 | ); 194 | }, 195 | )} 196 | 197 | 198 | )} 199 | 200 | 201 | ); 202 | }; 203 | -------------------------------------------------------------------------------- /webapp/src/components/MonthPicker/interface.ts: -------------------------------------------------------------------------------- 1 | import { IFirstRecordResult } from '../../interfaces'; 2 | 3 | export interface IMonthPickerValue { 4 | year: string; 5 | month: string; 6 | } 7 | 8 | export interface IMonthPickerProps { 9 | maxYear: string; 10 | maxMonth: string; 11 | currentYear: string; 12 | currentMonth: string; 13 | onSwitch?: (value: IMonthPickerValue) => void; 14 | } 15 | 16 | export interface IMonthPickerComponentProps extends IMonthPickerProps { 17 | minYear?: string; 18 | minMonth?: string; 19 | isLoading?: boolean; 20 | hasError?: boolean; 21 | } 22 | 23 | type FirstRecordData = Pick; 24 | 25 | export interface IMonthPickerQuery { 26 | FirstRecord: FirstRecordData; 27 | } 28 | 29 | export interface ICalendarMonth { 30 | text: string; 31 | month: string; 32 | year: string; 33 | isAvailable: boolean; 34 | } 35 | 36 | export interface ICalendarMonthPerYear { 37 | [key: string]: ICalendarMonth[]; 38 | } 39 | 40 | export interface IMonthsWithDataPerYear { 41 | [key: string]: string[]; 42 | } 43 | -------------------------------------------------------------------------------- /webapp/src/components/MonthPicker/query.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-boost'; 2 | 3 | export default gql` 4 | query getFirstRecord { 5 | FirstRecord { 6 | day 7 | } 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /webapp/src/components/MonthPicker/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import posed from 'react-pose'; 3 | import { QueryErrorIcon } from '../QueryErrorIcon'; 4 | import { COLORS } from '../../global-styles'; 5 | 6 | export const DIMENSIONS = { 7 | RETRACTED_HEIGHT: 45, 8 | RETRACTED_WIDTH: 175, 9 | EXPANDED_HEIGHT: 270, 10 | EXPANDED_WIDTH: 270, 11 | }; 12 | 13 | export const POSE_NAMES = { 14 | BG_EXPAND: 'bg-expand', 15 | BG_RETRACT: 'bg-retract', 16 | }; 17 | 18 | export const Root = styled.div` 19 | position: relative; 20 | width: ${DIMENSIONS.RETRACTED_WIDTH}px; 21 | height: ${DIMENSIONS.RETRACTED_HEIGHT}px; 22 | color: #e0e0e0; 23 | 24 | &:hover .scaled-bg { 25 | background-color: ${COLORS.DARK_GRAY}; 26 | } 27 | `; 28 | 29 | export const animatedScaleBgTransition = { 30 | default: { duration: 550, ease: [0.68, -0.25, 0.265, 1.15] }, 31 | }; 32 | export const enterExitTransition = { default: { duration: 300 } }; 33 | 34 | export const AnimatedRetractedTriggerBtn = posed.button({ 35 | enter: { 36 | opacity: 1, 37 | transition: enterExitTransition, 38 | y: 0, 39 | delay: animatedScaleBgTransition.default.duration + 100, 40 | }, 41 | exit: { 42 | opacity: 0, 43 | transition: enterExitTransition, 44 | y: -3, 45 | }, 46 | }); 47 | export const RetractedTriggerBtn = styled(AnimatedRetractedTriggerBtn)` 48 | position: absolute; 49 | top: 0; 50 | right: 0; 51 | display: flex; 52 | align-items: center; 53 | justify-content: center; 54 | flex-wrap: nowrap; 55 | cursor: pointer; 56 | z-index: 2; 57 | border: 0; 58 | background-color: transparent; 59 | font-size: 1em; 60 | width: ${DIMENSIONS.RETRACTED_WIDTH}px; 61 | height: ${DIMENSIONS.RETRACTED_HEIGHT}px; 62 | border-radius: 4px; 63 | color: currentColor; 64 | 65 | &:focus { 66 | outline: 0; 67 | } 68 | `; 69 | 70 | export const RetractedTriggerBtnText = styled.span` 71 | font-weight: 300; 72 | margin-right: 8px; 73 | white-space: nowrap; 74 | `; 75 | 76 | export const AnimatedPicker = posed.div({ 77 | enter: { 78 | opacity: 1, 79 | transition: enterExitTransition, 80 | y: 0, 81 | delay: animatedScaleBgTransition.default.duration + 100, 82 | }, 83 | exit: { opacity: 0, transition: enterExitTransition, y: -3 }, 84 | }); 85 | export const Picker = styled(AnimatedPicker)` 86 | position: absolute; 87 | top: 0; 88 | right: 0; 89 | z-index: 2; 90 | border: 0; 91 | background-color: transparent; 92 | font-size: 1em; 93 | width: ${DIMENSIONS.EXPANDED_WIDTH}px; 94 | height: ${DIMENSIONS.EXPANDED_HEIGHT}px; 95 | padding: 30px 15px 35px 15px; 96 | 97 | &:focus { 98 | outline: 0; 99 | } 100 | `; 101 | 102 | export const ExpandedTriggerBtn = styled.button` 103 | position: absolute; 104 | top: 5px; 105 | right: 5px; 106 | transition: opacity 300ms ease; 107 | width: 30px; 108 | height: 30px; 109 | padding: 0; 110 | 111 | &:focus { 112 | outline: 0; 113 | } 114 | 115 | &:focus, 116 | &:hover { 117 | opacity: 0.7; 118 | } 119 | `; 120 | 121 | export const PickerMonthContainer = styled.ul` 122 | width: 100%; 123 | height: 100%; 124 | display: grid; 125 | grid-template-columns: repeat(3, 1fr); 126 | column-gap: 10px; 127 | `; 128 | 129 | interface IPickerMonth { 130 | isAvailable: boolean; 131 | isCurrent: boolean; 132 | } 133 | export const PickerMonth = styled.li` 134 | display: flex; 135 | align-items: center; 136 | justify-content: center; 137 | font-size: 14px; 138 | transition: opacity 300ms ease, color 300ms ease; 139 | opacity: ${({ isAvailable }) => (isAvailable ? 1 : 0.4)}; 140 | cursor: ${({ isAvailable, isCurrent }) => { 141 | if (!isAvailable) { 142 | return 'not-allowed'; 143 | } 144 | 145 | if (isCurrent) { 146 | return 'default'; 147 | } 148 | 149 | return 'pointer'; 150 | }}; 151 | color: ${({ isCurrent }) => (isCurrent ? COLORS.CHART_BAR : 'currentColor')}; 152 | 153 | &:hover { 154 | ${({ isAvailable, isCurrent }) => 155 | isAvailable && 156 | !isCurrent && { 157 | opacity: 0.7, 158 | }} 159 | } 160 | `; 161 | 162 | export const AnimatedScaledBg = posed.div({ 163 | [POSE_NAMES.BG_EXPAND]: { 164 | width: DIMENSIONS.EXPANDED_WIDTH, 165 | height: DIMENSIONS.EXPANDED_HEIGHT, 166 | transition: animatedScaleBgTransition, 167 | }, 168 | [POSE_NAMES.BG_RETRACT]: { 169 | width: DIMENSIONS.RETRACTED_WIDTH, 170 | height: DIMENSIONS.RETRACTED_HEIGHT, 171 | transition: animatedScaleBgTransition, 172 | }, 173 | }); 174 | 175 | export const ScaledBg = styled(AnimatedScaledBg)` 176 | position: absolute; 177 | z-index: 1; 178 | top: 0; 179 | right: 0; 180 | width: ${DIMENSIONS.RETRACTED_WIDTH}px; 181 | height: ${DIMENSIONS.RETRACTED_HEIGHT}px; 182 | border-radius: 4px; 183 | background-color: ${COLORS.LIGHT_BLACK}; 184 | transform-origin: top right; 185 | border: 1px solid ${COLORS.GRAY}; 186 | transition: background-color 300ms ease; 187 | `; 188 | 189 | export const PickerYearContainer = styled.div` 190 | display: flex; 191 | align-items: center; 192 | justify-content: center; 193 | `; 194 | 195 | export const ErrorDisplay = styled(QueryErrorIcon)``; 196 | -------------------------------------------------------------------------------- /webapp/src/components/MonthPicker/use-calendar-data.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import moment from 'moment'; 3 | import { ICalendarMonthPerYear } from './interface'; 4 | import { MONTH_DATE_FORMAT } from '../../constants'; 5 | 6 | interface IUseCalendarDataParameters { 7 | isLoading: boolean; 8 | minYear: string; 9 | maxYear: string; 10 | maxMonth: string; 11 | minMonth?: string; 12 | } 13 | 14 | export function useCalendarData({ 15 | isLoading, 16 | minYear, 17 | maxYear, 18 | minMonth, 19 | maxMonth, 20 | }: IUseCalendarDataParameters) { 21 | const [availableYearList, setAvailableYearList] = useState([]); 22 | const [calendarMonthLabels, setCalendarMonthLabels] = useState< 23 | ICalendarMonthPerYear 24 | >({}); 25 | 26 | useEffect(() => { 27 | if (minMonth) { 28 | const startDate = moment(`${minYear}-${minMonth}`, MONTH_DATE_FORMAT); 29 | const endDate = moment(`${maxYear}-${maxMonth}`, MONTH_DATE_FORMAT).endOf( 30 | 'month', 31 | ); 32 | const minYearNumber = parseInt(minYear, 10); 33 | const maxYearNumber = parseInt(maxYear, 10); 34 | const yearCount = maxYearNumber - minYearNumber + 1; 35 | const yearsStartDate = moment(startDate).subtract(1, 'year'); 36 | 37 | const years = Array(yearCount) 38 | .fill(yearCount) 39 | .reduce( 40 | (accum: {}) => ({ 41 | ...accum, 42 | [yearsStartDate.add(1, 'year').format('YYYY')]: [], 43 | }), 44 | {}, 45 | ); 46 | 47 | const calendarMonthsStartDate = moment('12-01', 'MM-DD'); // starts at December because first iteration already adds 1 month 48 | const yearList = Object.keys(years); 49 | const calendarMonthsPerYear: ICalendarMonthPerYear = yearList.reduce( 50 | (accum: { [key: string]: any }, year: string) => { 51 | return { 52 | ...accum, 53 | [year]: Array(12) 54 | .fill(12) 55 | .map(() => { 56 | const referenceDate = calendarMonthsStartDate.add(1, 'month'); 57 | const month = referenceDate.format('MM'); 58 | const currentDate = moment( 59 | `${year}-${month}`, 60 | MONTH_DATE_FORMAT, 61 | ); 62 | const isAvailable = 63 | currentDate.isSameOrAfter(startDate) && 64 | currentDate.isSameOrBefore(endDate); 65 | 66 | return { 67 | year, 68 | month, 69 | text: referenceDate.format('MMM').toUpperCase(), 70 | isAvailable, 71 | }; 72 | }), 73 | }; 74 | }, 75 | {}, 76 | ); 77 | 78 | setAvailableYearList(yearList); 79 | setCalendarMonthLabels(calendarMonthsPerYear); 80 | } 81 | }, [minYear, maxYear, minMonth, maxMonth, isLoading]); 82 | 83 | return { 84 | availableYearList, 85 | calendarMonthLabels, 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /webapp/src/components/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Root, Link } from './styled'; 3 | 4 | export const Navigation: React.FC = () => { 5 | return ( 6 | 7 | 8 | Today 9 | 10 | 11 | Period 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /webapp/src/components/Navigation/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { NavLink } from 'react-router-dom'; 3 | import { COLORS } from '../../global-styles'; 4 | 5 | export const Root = styled.nav` 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | width: 100%; 10 | height: 50px; 11 | background-color: ${COLORS.LIGHT_BLACK}; 12 | border-bottom: 1px solid #aaa; 13 | `; 14 | 15 | export const Link = styled(NavLink)` 16 | &:visited { 17 | color: currentColor; 18 | } 19 | 20 | position: relative; 21 | top: -2px; 22 | opacity: 0.6; 23 | color: ${COLORS.WHITE}; 24 | text-decoration: none; 25 | font-size: 20px; 26 | transition: opacity 300ms ease-out; 27 | 28 | &::before, 29 | &::after { 30 | content: ''; 31 | position: absolute; 32 | width: 55%; 33 | height: 1px; 34 | bottom: -2px; 35 | background-color: currentColor; 36 | opacity: 0.85; 37 | transform: scaleX(0); 38 | transition: transform 300ms ease-out; 39 | } 40 | 41 | &::before { 42 | right: -5%; 43 | transform-origin: left; 44 | } 45 | 46 | &::after { 47 | left: -5%; 48 | transform-origin: right; 49 | } 50 | 51 | &:not(:last-child) { 52 | margin-right: 30px; 53 | } 54 | 55 | &.active { 56 | opacity: 1; 57 | 58 | &::before, 59 | &::after { 60 | transform: scaleX(1); 61 | } 62 | } 63 | 64 | &:hover:not(.active) { 65 | opacity: 0.8; 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /webapp/src/components/Period/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment, { Moment } from 'moment'; 3 | import { Card } from '../Card'; 4 | import { PeriodBarChart } from '../PeriodBarChart'; 5 | import { MonthPicker } from '../MonthPicker'; 6 | import { IMonthPickerValue } from '../MonthPicker/interface'; 7 | import { Averages } from '../Averages'; 8 | import { ChartContainer, Root, MonthPickerContainer } from './styled'; 9 | import { DATE_FORMAT, MONTH_DATE_FORMAT } from '../../constants'; 10 | 11 | const PERIOD_QUERY_STRING = 'p'; 12 | 13 | interface IPeriodProps { } 14 | 15 | const getPeriodEnd = (date: Moment): Moment => 16 | moment(date) 17 | .endOf('month') 18 | .add(1, 'day'); 19 | 20 | const getPeriodStart = (date: Moment): Moment => moment(date).startOf('month'); 21 | 22 | const today = moment(); 23 | let defaultPeriodStartDate = getPeriodStart(today); 24 | let defaultPeriodEndDate = getPeriodEnd(today); 25 | 26 | const urlParams = new URLSearchParams(window.location.search); 27 | 28 | for (const queryStringEntry of urlParams.entries()) { 29 | if (queryStringEntry[0] === PERIOD_QUERY_STRING) { 30 | const date = moment(queryStringEntry[1], MONTH_DATE_FORMAT); 31 | 32 | if (date.isValid()) { 33 | defaultPeriodStartDate = getPeriodStart(date); 34 | defaultPeriodEndDate = getPeriodEnd(date); 35 | } 36 | } 37 | } 38 | 39 | export const Period: React.FC = () => { 40 | const [periodStart, setPeriodStart] = React.useState( 41 | defaultPeriodStartDate.format(DATE_FORMAT), 42 | ); 43 | const [periodEnd, setPeriodEnd] = React.useState( 44 | defaultPeriodEndDate.format(DATE_FORMAT), 45 | ); 46 | const [currentSelectedMonth, setCurrentSelectedMonth] = React.useState( 47 | defaultPeriodStartDate.format('MM'), 48 | ); 49 | const [currentSelectedYear, setCurrentSelectedYear] = React.useState( 50 | defaultPeriodStartDate.format('YYYY'), 51 | ); 52 | const onPeriodSwitch = React.useCallback( 53 | ({ year, month }: IMonthPickerValue) => { 54 | const newPeriodStartDate = moment(`${year}-${month}-01`, DATE_FORMAT); 55 | const newPeriodEndDate = moment(newPeriodStartDate).endOf('month'); 56 | 57 | setPeriodStart(newPeriodStartDate.format(DATE_FORMAT)); 58 | setPeriodEnd(newPeriodEndDate.add(1, 'day').format(DATE_FORMAT)); 59 | setCurrentSelectedMonth(newPeriodStartDate.format('MM')); 60 | setCurrentSelectedYear(newPeriodStartDate.format('YYYY')); 61 | }, 62 | [], 63 | ); 64 | 65 | return ( 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /webapp/src/components/Period/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Root = styled.div` 4 | position: relative; 5 | width: 100%; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | `; 10 | 11 | export const MonthPickerContainer = styled.div` 12 | width: 100%; 13 | display: flex; 14 | justify-content: flex-end; 15 | margin-bottom: 40px; 16 | `; 17 | 18 | export const ChartContainer = styled.div` 19 | width: 100%; 20 | margin-bottom: 60px; 21 | `; 22 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/__mock__/data.ts: -------------------------------------------------------------------------------- 1 | export const data = [ 2 | { "day": "2019-12-02", "totalTimeAtOffice": { "hours": 8, "minutes": 12, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 47, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 1, "minutes": 4, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-03", "totalTimeAtOffice": { "hours": 8, "minutes": 22, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 59, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 52, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-04", "totalTimeAtOffice": { "hours": 9, "minutes": 17, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 54, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 52, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-05", "totalTimeAtOffice": { "hours": 8, "minutes": 31, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 1, "minutes": 8, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 1, "minutes": 1, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-06", "totalTimeAtOffice": { "hours": 7, "minutes": 55, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-09", "totalTimeAtOffice": { "hours": 8, "minutes": 1, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 58, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 58, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-10", "totalTimeAtOffice": { "hours": 8, "minutes": 19, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 48, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 52, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-11", "totalTimeAtOffice": { "hours": 8, "minutes": 47, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 1, "minutes": 8, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 51, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-12", "totalTimeAtOffice": { "hours": 7, "minutes": 47, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 54, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-13", "totalTimeAtOffice": { "hours": 8, "minutes": 28, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-16", "totalTimeAtOffice": { "hours": 8, "minutes": 20, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 53, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 1, "minutes": 1, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-17", "totalTimeAtOffice": { "hours": 8, "minutes": 40, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 47, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 1, "minutes": 3, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-18", "totalTimeAtOffice": { "hours": 8, "minutes": 11, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 52, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 56, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-19", "totalTimeAtOffice": { "hours": 7, "minutes": 25, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 55, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-20", "totalTimeAtOffice": { "hours": 8, "minutes": 2, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-23", "totalTimeAtOffice": { "hours": 8, "minutes": 49, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 50, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 1, "minutes": 1, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-24", "totalTimeAtOffice": { "hours": 7, "minutes": 31, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-27", "totalTimeAtOffice": { "hours": 8, "minutes": 53, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-30", "totalTimeAtOffice": { "hours": 7, "minutes": 24, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" }, { "day": "2019-12-31", "totalTimeAtOffice": { "hours": 6, "minutes": 54, "__typename": "TotalTimeAtOfficeResult" }, "totalMorningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalMorningCommuteTime" }, "totalEveningCommuteTime": { "hours": 0, "minutes": 0, "__typename": "TotalEveningCommuteTime" }, "__typename": "TimetableChartResult" } 3 | ]; -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/animations.ts: -------------------------------------------------------------------------------- 1 | import anime from 'animejs'; 2 | 3 | const TIMELINE_EASING = 'easeOutCubic'; 4 | const BARS_EASING = 'easeInOutCubic'; 5 | export const ANIMATION_IDS = { 6 | BAR_ANIMATED_RECTANGLE: 'BAR_RECT', 7 | BAR_Y_VALUE_LABEL: 'Y_VALUE', 8 | BAR_X_VALUE_LABEL: 'X_VALUE', 9 | }; 10 | 11 | const getAnimationTarget = (id: string) => `[data-animation-id="${id}"]`; 12 | 13 | export function createBarsInAnimationTimeline() { 14 | const animationTimeline = anime.timeline({ 15 | autoplay: false, 16 | easing: TIMELINE_EASING, 17 | }); 18 | 19 | // X value labels animation 20 | animationTimeline.add({ 21 | targets: getAnimationTarget(ANIMATION_IDS.BAR_X_VALUE_LABEL), 22 | duration: 200, 23 | opacity: [0, 1], 24 | scaleX: [0.875, 1], 25 | scaleY: [0.875, 1], 26 | translateY: [2, 0], 27 | }); 28 | 29 | // Chart bars animation 30 | animationTimeline.add({ 31 | targets: getAnimationTarget(ANIMATION_IDS.BAR_ANIMATED_RECTANGLE), 32 | duration: 800, 33 | translateY: ['100%', 0], 34 | delay: (el: any, i: number) => i * 30, 35 | easing: BARS_EASING, 36 | }); 37 | 38 | // Y value labels animation 39 | animationTimeline.add({ 40 | targets: getAnimationTarget(ANIMATION_IDS.BAR_Y_VALUE_LABEL), 41 | duration: 300, 42 | opacity: [0, 1], 43 | scaleX: [0.875, 1], 44 | scaleY: [0.875, 1], 45 | translateY: [2, 0], 46 | }); 47 | 48 | return animationTimeline; 49 | } 50 | 51 | export function createBarsOutAnimationTimeline() { 52 | const animationTimeline = anime.timeline({ 53 | autoplay: false, 54 | easing: TIMELINE_EASING, 55 | }); 56 | 57 | // Y value labels animation 58 | animationTimeline.add({ 59 | targets: getAnimationTarget(ANIMATION_IDS.BAR_Y_VALUE_LABEL), 60 | duration: 200, 61 | opacity: [1, 0], 62 | scaleX: [1, 0.875], 63 | scaleY: [1, 0.875], 64 | translateY: [0, 2], 65 | }); 66 | 67 | // Chart bars animation 68 | animationTimeline.add({ 69 | targets: getAnimationTarget(ANIMATION_IDS.BAR_ANIMATED_RECTANGLE), 70 | duration: 600, 71 | translateY: [0, '100%'], 72 | easing: BARS_EASING, 73 | }); 74 | 75 | // X value labels animation 76 | animationTimeline.add({ 77 | targets: getAnimationTarget(ANIMATION_IDS.BAR_X_VALUE_LABEL), 78 | duration: 200, 79 | opacity: [1, 0], 80 | scaleX: [1, 0.875], 81 | scaleY: [1, 0.875], 82 | translateY: [0, 2], 83 | }); 84 | 85 | return animationTimeline; 86 | } 87 | 88 | export function createReverseBarsOutAnimationTimeline() { 89 | const animationTimeline = anime.timeline({ 90 | autoplay: false, 91 | easing: TIMELINE_EASING, 92 | }); 93 | 94 | // Y value labels animation 95 | animationTimeline.add({ 96 | targets: getAnimationTarget(ANIMATION_IDS.BAR_Y_VALUE_LABEL), 97 | duration: 50, 98 | opacity: 0, 99 | scaleX: 0.875, 100 | scaleY: 0.875, 101 | translateY: 2, 102 | }); 103 | 104 | // Chart bars animation 105 | animationTimeline.add({ 106 | targets: getAnimationTarget(ANIMATION_IDS.BAR_ANIMATED_RECTANGLE), 107 | duration: 450, 108 | translateY: '100%', 109 | easing: 'easeInOutCubic', 110 | }); 111 | 112 | // X value labels animation 113 | animationTimeline.add({ 114 | targets: getAnimationTarget(ANIMATION_IDS.BAR_X_VALUE_LABEL), 115 | duration: 100, 116 | opacity: [1, 0], 117 | scaleX: [1, 0.875], 118 | scaleY: [1, 0.875], 119 | translateY: [0, 2], 120 | }); 121 | 122 | return animationTimeline; 123 | } 124 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/carousel-chart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Slider } from '../Slider'; 3 | import { BarsContainer } from './styled'; 4 | import { ICarouselChartProps } from './interface'; 5 | 6 | export const BARS_PER_PAGE = 5; 7 | export const SLIDER_SPEED = 800; 8 | const SLIDER_EASING = 'cubic-bezier(0.645, 0.045, 0.355, 1)'; // easeInOutCubic 9 | 10 | const CarouselChartComponent = React.forwardRef( 11 | (props, ref) => { 12 | const { numberOfSlides, chartData, renderChartBars } = props; 13 | const slides = Array(numberOfSlides) 14 | .fill(numberOfSlides) 15 | .map((current, i) => { 16 | const currentPage = i + 1; 17 | 18 | return ( 19 | 24 | {chartData 25 | .slice( 26 | currentPage * BARS_PER_PAGE - BARS_PER_PAGE, 27 | currentPage * BARS_PER_PAGE, 28 | ) 29 | .map(renderChartBars)} 30 | 31 | ); 32 | }); 33 | 34 | return ( 35 | 43 | {slides} 44 | 45 | ); 46 | }, 47 | ); 48 | 49 | export const CarouselChart = React.memo(CarouselChartComponent); 50 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/chart-bar.tsx: -------------------------------------------------------------------------------- 1 | import { formatMinutes, getBarHeight, getTotalMinutesFromTime } from './utils'; 2 | import { 3 | BarChartXValue, 4 | BarChartYValueLabel, 5 | BarContainer, 6 | BarRectangle, 7 | BarRectangleContainer, 8 | DIMENSIONS, 9 | } from './styled'; 10 | import { ANIMATION_IDS } from './animations'; 11 | import moment from 'moment'; 12 | import React from 'react'; 13 | import { IChartBarProps } from './interface'; 14 | 15 | export const ChartBar = React.forwardRef( 16 | ( 17 | { day, hours, minutes, chartDataMaxYValue, barWidth, isMobileView }, 18 | ref, 19 | ) => { 20 | const totalMinutes = getTotalMinutesFromTime({ hours, minutes }); 21 | const shouldDisplayYValue = !(hours === 0 && minutes < 30); 22 | const height = getBarHeight( 23 | DIMENSIONS.CHART_HEIGHT, 24 | chartDataMaxYValue, 25 | totalMinutes, 26 | ); 27 | 28 | return ( 29 | 30 | 31 | 35 | 36 | 37 | {shouldDisplayYValue && ( 38 | 41 | {hours}h{formatMinutes(minutes)} 42 | 43 | )} 44 | 45 | 49 | {moment(day).format('DD/MM')} 50 | 51 | 52 | ); 53 | }, 54 | ); 55 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import debounce from 'lodash.debounce'; 3 | import { useQuery } from '@apollo/react-hooks'; 4 | import usePrevious from 'react-hooks-use-previous'; 5 | import { 6 | IPeriodChartProps, 7 | IPeriodChartComponentProps, 8 | IPeriodQueryData, 9 | IChartData, 10 | } from './interface'; 11 | import { 12 | getTotalMinutesFromTime, 13 | getArrayMaxValue, 14 | } from './utils'; 15 | import query from './query'; 16 | import { StatusInformation } from './status-information'; 17 | import { ITimetableChartResult } from '../../interfaces'; 18 | import { 19 | createBarsInAnimationTimeline, 20 | createBarsOutAnimationTimeline, 21 | createReverseBarsOutAnimationTimeline, 22 | } from './animations'; 23 | import { BARS_PER_PAGE, SLIDER_SPEED, CarouselChart } from './carousel-chart'; 24 | import { ChartBar } from "./chart-bar"; 25 | import { 26 | DIMENSIONS, 27 | Root, 28 | BarsContainer, 29 | StatusInformationContainer, 30 | BarChartAxis, 31 | ChartBarsSlider, 32 | } from './styled'; 33 | 34 | const chartContainerRef = React.createRef(); 35 | const sliderRef = React.createRef(); 36 | const BAR_WIDTH_INITIAL_VALUE = -1; 37 | 38 | export const PeriodBarChart: React.FC = ({ 39 | periodStart, 40 | periodEnd, 41 | }) => { 42 | const { loading, data, error } = useQuery(query, { 43 | variables: { 44 | periodStart, 45 | periodEnd, 46 | }, 47 | }); 48 | const periodId = `${periodStart}_${periodEnd}`; 49 | 50 | if (!loading && data && data.Period) { 51 | const { 52 | Period: { timetableChart }, 53 | } = data; 54 | 55 | return ( 56 | 57 | ); 58 | } 59 | 60 | return ( 61 | 66 | ); 67 | }; 68 | 69 | const DATA_INITIAL_PROP: any[] = []; 70 | 71 | export const PeriodBarChartComponent: React.FC = ({ 72 | data = DATA_INITIAL_PROP, 73 | periodId, 74 | isLoading, 75 | hasError, 76 | }) => { 77 | const animateBarsInTimeline = React.useRef(); 78 | const [isAnimating, setIsAnimating] = React.useState(false); 79 | const [chartData, setChartData] = React.useState(data); 80 | const [areBarsRendered, setAreBarsRendered] = React.useState(false); 81 | const [chartDoneAnimating, setChartDoneAnimating] = React.useState( 82 | false, 83 | ); 84 | const [isMobileView, setIsMobileView] = React.useState(false); 85 | const [barWidth, setBarWidth] = React.useState( 86 | BAR_WIDTH_INITIAL_VALUE, 87 | ); 88 | const [windowWidth, setWindowWidth] = React.useState( 89 | window.innerWidth, 90 | ); 91 | const numberOfSlides = Math.ceil(chartData.length / BARS_PER_PAGE); 92 | const previousPeriodId = usePrevious(periodId, periodId); 93 | const hasPeriodChanged = periodId !== previousPeriodId; 94 | const isDataLoaded = data === chartData; 95 | const noData = data.length === 0; 96 | const chartDataMaxYValue = React.useMemo(() => { 97 | return getArrayMaxValue(chartData, (day: any) => 98 | getTotalMinutesFromTime(day.totalTimeAtOffice), 99 | ); 100 | }, [chartData]); 101 | 102 | React.useEffect(function windowResizeHook() { 103 | const onResize = debounce(() => setWindowWidth(window.innerWidth), 100); 104 | 105 | window.addEventListener('resize', onResize); 106 | 107 | return () => window.removeEventListener('resize', onResize); 108 | }, []); 109 | 110 | const renderChartBars = React.useCallback( 111 | ({ 112 | totalTimeAtOffice, 113 | day, 114 | }: ITimetableChartResult) => { 115 | const handleBarRef = (node: HTMLDivElement) => { 116 | if (node === null) { 117 | return setAreBarsRendered(false); 118 | } 119 | 120 | setAreBarsRendered(true); 121 | }; 122 | 123 | return ; 124 | }, 125 | [chartDataMaxYValue, barWidth, isMobileView], 126 | ); 127 | 128 | React.useEffect( 129 | function barWidthCalculationEffect() { 130 | if (chartContainerRef.current && chartData.length) { 131 | const { offsetWidth } = chartContainerRef.current; 132 | const predictedBarWidth = Math.round( 133 | offsetWidth / chartData.length - DIMENSIONS.BAR_GUTTER, 134 | ); 135 | 136 | if (predictedBarWidth <= DIMENSIONS.MIN_BAR_WIDTH) { 137 | const mobilePredictedBarWidth = Math.round( 138 | offsetWidth / BARS_PER_PAGE - DIMENSIONS.BAR_GUTTER, 139 | ); 140 | 141 | setBarWidth(mobilePredictedBarWidth); 142 | setIsMobileView(true); 143 | 144 | return; 145 | } 146 | 147 | setBarWidth(predictedBarWidth); 148 | setIsMobileView(false); 149 | } 150 | }, 151 | [windowWidth, periodId, chartData], 152 | ); 153 | 154 | React.useEffect( 155 | function mobileViewSwitchEffect() { 156 | setChartDoneAnimating(false); 157 | }, 158 | [isMobileView], 159 | ); 160 | 161 | React.useEffect( 162 | function reverseAnimationEffect() { 163 | if (hasPeriodChanged && isAnimating && !chartDoneAnimating) { 164 | animateBarsInTimeline.current.pause(); 165 | 166 | const animationTimeline = createReverseBarsOutAnimationTimeline(); 167 | 168 | animationTimeline.complete = () => { 169 | sliderRef.current && sliderRef.current.slickGoTo(0, true); 170 | 171 | window.requestIdleCallback(() => { 172 | setIsAnimating(false); 173 | setChartDoneAnimating(false); 174 | }); 175 | }; 176 | 177 | setIsAnimating(true); 178 | window.requestIdleCallback(animationTimeline.play); 179 | } 180 | }, 181 | [hasPeriodChanged, data, chartDoneAnimating, isAnimating], 182 | ); 183 | 184 | React.useEffect( 185 | function animateBarsOut() { 186 | if (hasPeriodChanged && chartDoneAnimating && data !== chartData) { 187 | const animationTimeline = createBarsOutAnimationTimeline(); 188 | 189 | animationTimeline.complete = () => { 190 | sliderRef.current && sliderRef.current.slickGoTo(0, true); 191 | 192 | window.requestIdleCallback(() => { 193 | setIsAnimating(false); 194 | setChartDoneAnimating(false); 195 | }); 196 | }; 197 | 198 | setIsAnimating(true); 199 | window.requestIdleCallback(animationTimeline.play); 200 | } 201 | }, 202 | [chartDoneAnimating, data, chartData, hasPeriodChanged], 203 | ); 204 | 205 | React.useEffect( 206 | function animateBarsInEffect() { 207 | if ( 208 | areBarsRendered && 209 | isDataLoaded && 210 | chartData.length && 211 | !chartDoneAnimating 212 | ) { 213 | const animationTimeline = createBarsInAnimationTimeline(); 214 | 215 | animateBarsInTimeline.current = animationTimeline; 216 | animationTimeline.complete = () => { 217 | if (isMobileView) { 218 | window.requestIdleCallback( 219 | () => 220 | sliderRef.current && 221 | sliderRef.current.slickGoTo(numberOfSlides), 222 | ); 223 | 224 | setTimeout( 225 | () => 226 | window.requestIdleCallback(() => { 227 | setIsAnimating(false); 228 | setChartDoneAnimating(true); 229 | }), 230 | SLIDER_SPEED, 231 | ); 232 | } else { 233 | window.requestIdleCallback(() => { 234 | setIsAnimating(false); 235 | setChartDoneAnimating(true); 236 | }); 237 | } 238 | }; 239 | 240 | setIsAnimating(true); 241 | window.requestIdleCallback(animationTimeline.play); 242 | } 243 | }, 244 | [ 245 | areBarsRendered, 246 | chartDoneAnimating, 247 | data, 248 | chartData, 249 | isMobileView, 250 | numberOfSlides, 251 | isDataLoaded, 252 | ], 253 | ); 254 | 255 | React.useEffect( 256 | function freshDataEffect() { 257 | if (!chartDoneAnimating && !isAnimating) { 258 | setChartData(data); 259 | } 260 | }, 261 | [chartDoneAnimating, isAnimating, data], 262 | ); 263 | 264 | if (isMobileView) { 265 | return ( 266 | 267 | 268 | {!chartDoneAnimating && ( 269 | 270 | 275 | 276 | )} 277 | 278 | 284 | 285 | 286 | 287 | 288 | ); 289 | } 290 | 291 | return ( 292 | 293 | {!chartDoneAnimating && ( 294 | 295 | 300 | 301 | )} 302 | 303 | {chartData.map(renderChartBars)} 304 | 305 | 306 | 307 | ); 308 | }; 309 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/interface.ts: -------------------------------------------------------------------------------- 1 | import {IPeriodResult, ITime, ITimetableChartResult} from '../../interfaces'; 2 | 3 | type RequestIdleCallbackHandle = any; 4 | type RequestIdleCallbackOptions = { 5 | timeout: number; 6 | }; 7 | type RequestIdleCallbackDeadline = { 8 | readonly didTimeout: boolean; 9 | timeRemaining: () => number; 10 | }; 11 | 12 | declare global { 13 | interface Window { 14 | requestIdleCallback: ( 15 | callback: (deadline: RequestIdleCallbackDeadline) => void, 16 | opts?: RequestIdleCallbackOptions, 17 | ) => RequestIdleCallbackHandle; 18 | cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void; 19 | } 20 | } 21 | 22 | export interface IPeriodChartProps { 23 | periodStart: string; 24 | periodEnd: string; 25 | } 26 | 27 | type PeriodQueryData = Pick; 28 | 29 | export interface IPeriodQueryData { 30 | Period: PeriodQueryData; 31 | } 32 | 33 | export type IChartData = ITimetableChartResult[]; 34 | 35 | export interface IPeriodChartComponentProps { 36 | data?: IChartData; 37 | periodId: string; 38 | isLoading?: boolean; 39 | hasError?: boolean; 40 | } 41 | 42 | export interface IStatusInfoProps 43 | extends Pick { 44 | noData?: boolean; 45 | } 46 | 47 | export interface ICarouselChartProps { 48 | numberOfSlides: number; 49 | chartData: IChartData; 50 | renderChartBars(chartResult: ITimetableChartResult): JSX.Element; 51 | } 52 | 53 | export interface IChartBarProps { 54 | barWidth: number; 55 | hours: number; 56 | minutes: number; 57 | chartDataMaxYValue: number; 58 | isMobileView: boolean; 59 | day: string; 60 | } 61 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/query.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-boost'; 2 | 3 | export default gql` 4 | query getPeriod($periodStart: String!, $periodEnd: String!) { 5 | Period(periodStart: $periodStart, periodEnd: $periodEnd) { 6 | timetableChart { 7 | day 8 | 9 | totalTimeAtOffice { 10 | hours 11 | minutes 12 | } 13 | totalMorningCommuteTime { 14 | hours 15 | minutes 16 | } 17 | totalEveningCommuteTime { 18 | hours 19 | minutes 20 | } 21 | } 22 | } 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/status-information.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import posed, {PoseGroup} from "react-pose"; 3 | import styled from '@emotion/styled'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faEmptySet } from '@fortawesome/pro-regular-svg-icons'; 6 | import { faIcon as errorIcon } from '../QueryErrorIcon'; 7 | import { LoadingSpinner } from '../LoadingSpinner'; 8 | import { IStatusInfoProps } from './interface'; 9 | 10 | const AnimatedRoot = posed.div({ 11 | enter: { opacity: 1, delay: 300 }, 12 | exit: { opacity: 0 }, 13 | }); 14 | 15 | interface IRootProps { 16 | width?: number; 17 | } 18 | 19 | const Root = styled(AnimatedRoot)` 20 | position: absolute; 21 | top: 50%; 22 | left: 50%; 23 | transform: translate(-50%, -50%); 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: center; 28 | text-align: center; 29 | width: ${({ width }) => `${width}px` || 'auto'} 30 | `; 31 | 32 | const Icon = styled(FontAwesomeIcon)` 33 | font-size: 60px; 34 | margin-bottom: 16px; 35 | `; 36 | 37 | const Text = styled.div` 38 | font-size: 18px; 39 | `; 40 | 41 | export const StatusInformation: React.FC = ({ 42 | noData, 43 | hasError, 44 | isLoading, 45 | }) => { 46 | const renderContent = React.useCallback(() => { 47 | if (isLoading) { 48 | return ( 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | if (hasError) { 56 | return ( 57 | 58 | 59 | 60 | There was a problem while fetching data for the selected period 61 | 62 | 63 | ); 64 | } 65 | 66 | if (noData) { 67 | return ( 68 | 69 | 70 | No recorded data for the selected period 71 | 72 | ); 73 | } 74 | 75 | return undefined; 76 | }, [isLoading, hasError, noData]); 77 | 78 | return ( 79 | 80 | {renderContent()} 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import posed from 'react-pose'; 3 | import { COLORS } from '../../global-styles'; 4 | 5 | export const DIMENSIONS = { 6 | CHART_HEIGHT: 375, 7 | BAR_GUTTER: 8, 8 | MIN_BAR_WIDTH: 25, 9 | }; 10 | 11 | export const Root = styled.div` 12 | position: relative; 13 | `; 14 | 15 | export const StatusInformationContainer = styled.div` 16 | position: absolute; 17 | height: 100%; 18 | width: 100%; 19 | font-size: 40px; 20 | `; 21 | 22 | export const ChartBarsSlider = styled.div` 23 | height: ${DIMENSIONS.CHART_HEIGHT}px; 24 | `; 25 | 26 | export const AnimatedBarLabel = posed.div({ 27 | visible: { 28 | opacity: 1, 29 | scaleX: 1, 30 | scaleY: 1, 31 | y: 0, 32 | transition: { 33 | default: { duration: 350, ease: [0.215, 0.61, 0.355, 1] }, 34 | }, 35 | }, 36 | invisible: { 37 | opacity: 0, 38 | scaleX: 0.875, 39 | scaleY: 0.875, 40 | y: 2, 41 | }, 42 | }); 43 | 44 | export const BarChartYValueLabel = styled.div` 45 | position: absolute; 46 | width: 100%; 47 | top: 7px; 48 | left: 0; 49 | font-size: 1em; 50 | text-align: center; 51 | transform-origin: center center; 52 | will-change: transform; 53 | opacity: 0; 54 | `; 55 | 56 | interface IBarChartXValueProps { 57 | isMobile?: boolean; 58 | } 59 | 60 | export const BarChartXValue = styled.div` 61 | position: absolute; 62 | width: 100%; 63 | bottom: ${({ isMobile }) => (isMobile ? '5px' : '-26px')}; 64 | left: 0; 65 | font-size: 1.2em; 66 | text-align: center; 67 | transform-origin: center center; 68 | will-change: transform; 69 | opacity: 0; 70 | `; 71 | 72 | interface IBarsContainerProps { 73 | isCarouselItem?: boolean; 74 | isCentered?: boolean; 75 | } 76 | 77 | export const BarsContainer = styled.div` 78 | display: ${({ isCarouselItem }) => 79 | isCarouselItem ? 'inline-flex !important' : 'flex'}; 80 | align-items: flex-end; 81 | justify-content: ${({ isCentered }) => 82 | isCentered ? 'flex-start' : 'center'}; 83 | padding: 0 ${DIMENSIONS.BAR_GUTTER / 2}px; 84 | height: ${DIMENSIONS.CHART_HEIGHT}px; 85 | outline: 0; 86 | `; 87 | 88 | function getBarContainerFontSize(barWidth: number) { 89 | if (barWidth >= 50) { 90 | return '12px'; 91 | } 92 | 93 | if (barWidth >= 40) { 94 | return '11px'; 95 | } 96 | 97 | if (barWidth >= 30) { 98 | return '10px'; 99 | } 100 | 101 | return '9px'; 102 | } 103 | 104 | interface IBarContainerProps { 105 | barWidth: number; 106 | } 107 | 108 | export const BarContainer = styled.div` 109 | position: relative; 110 | flex: 1; 111 | font-size: ${({ barWidth }) => getBarContainerFontSize(barWidth)}; 112 | width: ${({ barWidth }) => barWidth}px; 113 | max-width: 100px; 114 | 115 | &:not(:first-of-type) { 116 | margin-left: ${DIMENSIONS.BAR_GUTTER}px; 117 | } 118 | `; 119 | 120 | interface IBarRectangleContainerProps { 121 | barHeight: number; 122 | } 123 | 124 | export const BarRectangleContainer = styled.div` 125 | height: ${({ barHeight }) => barHeight}px; 126 | overflow: hidden; 127 | `; 128 | 129 | export const AnimatedBar = posed.div({ 130 | visible: { 131 | y: 0, 132 | transition: ({ index }: { index: number }) => ({ 133 | y: { 134 | duration: 900, 135 | ease: [0.645, 0.045, 0.355, 1], 136 | delay: index * 30, 137 | }, 138 | }), 139 | }, 140 | invisible: { y: '100%' }, 141 | }); 142 | 143 | export const BarRectangle = styled.div` 144 | height: 100%; 145 | border-top-left-radius: 5px; 146 | border-top-right-radius: 5px; 147 | background-color: ${COLORS.CHART_BAR}; 148 | transform-origin: bottom left; 149 | will-change: transform; 150 | transform: translateY(100%); 151 | `; 152 | 153 | export const BarChartAxis = styled.div` 154 | height: 4px; 155 | width: 100%; 156 | background-color: ${COLORS.GRAY}; 157 | border-radius: 8px; 158 | `; 159 | -------------------------------------------------------------------------------- /webapp/src/components/PeriodBarChart/utils.ts: -------------------------------------------------------------------------------- 1 | import { ITime } from '../../interfaces'; 2 | 3 | export function getArrayMaxValue(array: any[], acessor: Function): number { 4 | return array.reduce((accum: number, currentItem: any) => { 5 | const value = acessor(currentItem); 6 | 7 | if (value > accum) { 8 | return value; 9 | } 10 | 11 | return accum; 12 | }, 0); 13 | } 14 | 15 | export function getTotalMinutesFromTime(time: ITime): number { 16 | return time.hours * 60 + time.minutes; 17 | } 18 | 19 | export function getBarHeight( 20 | maxHeight: number, 21 | maxValue: number, 22 | value: number, 23 | ): number { 24 | return (value * maxHeight) / maxValue; 25 | } 26 | 27 | export function formatMinutes(num: number): string { 28 | if (num < 10) { 29 | return `0${num}`; 30 | } 31 | 32 | return num.toString(); 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/components/QueryErrorIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faExclamationCircle } from '@fortawesome/pro-regular-svg-icons'; 4 | import { Root } from './styled'; 5 | 6 | export const faIcon = faExclamationCircle; 7 | export const QueryErrorIcon: React.FC<{ className?: string }> = ({ 8 | className, 9 | }) => ( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /webapp/src/components/QueryErrorIcon/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { COLORS } from '../../global-styles'; 3 | 4 | export const Root = styled.span` 5 | color: ${COLORS.DANGER}; 6 | font-size: 18px; 7 | `; 8 | -------------------------------------------------------------------------------- /webapp/src/components/Section/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ISectionProps } from './interface'; 3 | import { Content } from './styled'; 4 | 5 | export const Section: React.FC = ({ children }) => { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /webapp/src/components/Section/interface.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | export interface ISectionProps { 4 | children: ReactNode; 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/components/Section/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Content = styled.div` 4 | display: flex; 5 | align-items: center; 6 | 7 | @media (max-width: 960px) { 8 | justify-content: center; 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /webapp/src/components/Slider/index.ts: -------------------------------------------------------------------------------- 1 | import SlickSlider from 'react-slick'; 2 | import './slick.css'; 3 | 4 | export const Slider = SlickSlider; 5 | -------------------------------------------------------------------------------- /webapp/src/components/Slider/slick.css: -------------------------------------------------------------------------------- 1 | .slick-list, 2 | .slick-slider, 3 | .slick-track { 4 | position: relative; 5 | display: block; 6 | } 7 | .slick-loading .slick-slide, 8 | .slick-loading .slick-track { 9 | visibility: hidden; 10 | } 11 | .slick-slider { 12 | box-sizing: border-box; 13 | -webkit-user-select: none; 14 | -moz-user-select: none; 15 | -ms-user-select: none; 16 | user-select: none; 17 | -webkit-touch-callout: none; 18 | -khtml-user-select: none; 19 | -ms-touch-action: pan-y; 20 | touch-action: pan-y; 21 | -webkit-tap-highlight-color: transparent; 22 | } 23 | .slick-list { 24 | overflow: hidden; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | .slick-list:focus { 29 | outline: 0; 30 | } 31 | .slick-list.dragging { 32 | cursor: pointer; 33 | cursor: hand; 34 | } 35 | .slick-slider .slick-list, 36 | .slick-slider .slick-track { 37 | -webkit-transform: translate3d(0, 0, 0); 38 | -moz-transform: translate3d(0, 0, 0); 39 | -ms-transform: translate3d(0, 0, 0); 40 | -o-transform: translate3d(0, 0, 0); 41 | transform: translate3d(0, 0, 0); 42 | } 43 | .slick-track { 44 | top: 0; 45 | left: 0; 46 | } 47 | .slick-track:after, 48 | .slick-track:before { 49 | display: table; 50 | content: ''; 51 | } 52 | .slick-track:after { 53 | clear: both; 54 | } 55 | .slick-slide { 56 | display: none; 57 | float: left; 58 | height: 100%; 59 | min-height: 1px; 60 | } 61 | [dir='rtl'] .slick-slide { 62 | float: right; 63 | } 64 | .slick-slide img { 65 | display: block; 66 | } 67 | .slick-slide.slick-loading img { 68 | display: none; 69 | } 70 | .slick-slide.dragging img { 71 | pointer-events: none; 72 | } 73 | .slick-initialized .slick-slide { 74 | display: block; 75 | } 76 | .slick-vertical .slick-slide { 77 | display: block; 78 | height: auto; 79 | border: 1px solid transparent; 80 | } 81 | .slick-arrow.slick-hidden { 82 | display: none; 83 | } 84 | 85 | /* overrides */ 86 | 87 | .slick-slide, 88 | .slick-slide > div { 89 | outline: 0 !important; 90 | } 91 | 92 | .slick-dots { 93 | display: flex !important; 94 | margin: 15px 0 0 0; 95 | padding: 0; 96 | align-items: center; 97 | justify-content: center; 98 | list-style: none; 99 | } 100 | 101 | .slick-dots button { 102 | display: none; 103 | } 104 | 105 | .slick-dots li { 106 | width: 6px; 107 | height: 6px; 108 | border-radius: 50%; 109 | transition: background-color 170ms ease, transform 170ms ease; 110 | background-color: #aaa; 111 | } 112 | 113 | .slick-dots li:not(:first-of-type) { 114 | margin-left: 5px; 115 | } 116 | 117 | .slick-dots li.slick-active { 118 | transform: scale(1.25); 119 | background-color: #eee; 120 | } 121 | -------------------------------------------------------------------------------- /webapp/src/components/TimeDisplay/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Skeleton from 'react-loading-skeleton'; 3 | import { ITimeDisplayProps } from './interface'; 4 | import { UnitDisplay, Root, Unit } from './styled'; 5 | 6 | export const TimeDisplay: React.FC = ({ 7 | hours = 0, 8 | minutes = 0, 9 | isLoading, 10 | }) => { 11 | return ( 12 | 13 | 14 | {isLoading ? ( 15 | 16 | ) : ( 17 | <> 18 | {hours > 0 && ( 19 | 20 | {hours} 21 | hrs. 22 | 23 | )}{' '} 24 | 25 | {minutes} 26 | min. 27 | 28 | 29 | )} 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /webapp/src/components/TimeDisplay/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ITimeDisplayProps { 2 | hours?: number; 3 | minutes?: number; 4 | isLoading?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /webapp/src/components/TimeDisplay/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Root = styled.div` 4 | display: flex; 5 | `; 6 | 7 | export const Unit = styled.span` 8 | font-size: 0.6em; 9 | `; 10 | 11 | export const UnitDisplay = styled.div` 12 | font-size: 28px; 13 | font-weight: bold; 14 | `; 15 | -------------------------------------------------------------------------------- /webapp/src/components/TimetableDisplay/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import Skeleton from 'react-loading-skeleton'; 5 | import { ITimetableDisplayProps } from './interface'; 6 | import { 7 | IconContainer, 8 | Root, 9 | DisplayIconContainer, 10 | ErrorDisplay, 11 | Timetable, 12 | TimetableLabel, 13 | TimetableTimestamp, 14 | } from './styled'; 15 | 16 | export const TimetableDisplay: React.FC = ({ 17 | icon, 18 | timetables, 19 | isLoading, 20 | hasError, 21 | }) => { 22 | return ( 23 | 24 | 25 | 26 | {hasError && } 27 | 28 | 29 | 30 | 31 |
    32 | {timetables.map(({ timestamp, label }) => { 33 | const timetableDate = moment(timestamp, 'HH:mm:ssZ'); 34 | const timetableLabel = `${label}: `; 35 | 36 | if (timetableDate && !timetableDate.isValid()) { 37 | return ( 38 | 39 | 40 | {isLoading ? : timetableLabel} 41 | 42 | 43 | {isLoading ? : 'n/a'} 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | {timetableLabel} 52 | 53 | {timetableDate.format('HH:mm')} 54 | 55 | 56 | ); 57 | })} 58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /webapp/src/components/TimetableDisplay/interface.ts: -------------------------------------------------------------------------------- 1 | import { IconProp } from '@fortawesome/fontawesome-svg-core'; 2 | 3 | export interface ITimetable { 4 | timestamp?: string; 5 | label: string; 6 | } 7 | 8 | export interface ITimetableDisplayProps { 9 | icon: IconProp; 10 | timetables: ITimetable[]; 11 | isLoading?: boolean; 12 | hasError?: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /webapp/src/components/TimetableDisplay/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { QueryErrorIcon } from '../QueryErrorIcon'; 3 | 4 | export const CSS_VARIABLES = { 5 | LABEL_MIN_WIDTH: 125, 6 | MQ: { X_SMALL: 375 }, 7 | }; 8 | 9 | export const Root = styled.div` 10 | display: flex; 11 | 12 | @media (max-width: ${CSS_VARIABLES.MQ.X_SMALL}px) { 13 | flex-direction: column; 14 | } 15 | `; 16 | 17 | export const IconContainer = styled.div` 18 | display: flex; 19 | align-items: center; 20 | padding: 0 30px 0 0; 21 | margin-right: 34px; 22 | border-right: 1px solid #aaa; 23 | font-size: 42px; 24 | 25 | @media (max-width: ${CSS_VARIABLES.MQ.X_SMALL}px) { 26 | border-right: none; 27 | padding: 0 0 20px 0; 28 | margin: 0; 29 | justify-content: center; 30 | } 31 | `; 32 | 33 | interface ITimetableProps { 34 | isLoading?: boolean; 35 | } 36 | export const Timetable = styled.li` 37 | display: ${({ isLoading }) => (isLoading ? 'initial' : 'flex')}; 38 | align-items: center; 39 | padding: 7px 0; 40 | 41 | &:not(:last-child) { 42 | border-bottom: 1px solid #aaa; 43 | } 44 | 45 | @media (max-width: ${CSS_VARIABLES.MQ.X_SMALL}px) { 46 | justify-content: center; 47 | } 48 | `; 49 | 50 | export const TimetableLabel = styled.span` 51 | display: inline-block; 52 | min-width: ${CSS_VARIABLES.LABEL_MIN_WIDTH}px; 53 | font-size: 0.75em; 54 | font-size: 14px; 55 | opacity: 0.9; 56 | `; 57 | 58 | export const TimetableTimestamp = styled.span` 59 | font-weight: bold; 60 | `; 61 | 62 | export const DisplayIconContainer = styled.div` 63 | position: relative; 64 | `; 65 | 66 | export const ErrorDisplay = styled(QueryErrorIcon)` 67 | position: absolute; 68 | top: -12px; 69 | right: -12px; 70 | `; 71 | -------------------------------------------------------------------------------- /webapp/src/components/Today/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from '../Card'; 3 | import { DayTimetable } from '../DayTimetable'; 4 | import { DayTotal } from '../DayTotal'; 5 | import { Root, DayTimetableContainer, DayTotalContainerCard } from './styled'; 6 | 7 | export const Today: React.FC = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /webapp/src/components/Today/styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { Card } from '../Card'; 3 | 4 | export const CSS_VARS = { 5 | GRID_GUTTER: 32, 6 | MQ: { 7 | MEDIUM: 960, 8 | SMALL: 620, 9 | X_SMALL: 375, 10 | }, 11 | }; 12 | 13 | export const Root = styled.div` 14 | display: flex; 15 | align-items: stretch; 16 | justify-content: center; 17 | width: 100%; 18 | 19 | @media (max-width: ${CSS_VARS.MQ.MEDIUM}px) { 20 | flex-direction: column; 21 | align-items: center; 22 | } 23 | `; 24 | 25 | export const DayTimetableContainer = styled.div` 26 | margin-right: 40px; 27 | flex: 0 1 auto; 28 | 29 | @media (max-width: ${CSS_VARS.MQ.MEDIUM}px) { 30 | margin-right: 0; 31 | margin-bottom: 40px; 32 | } 33 | 34 | @media (max-width: ${CSS_VARS.MQ.X_SMALL}px) { 35 | width: 100%; 36 | max-width: 250px; 37 | } 38 | `; 39 | 40 | export const DayTotalContainerCard = styled(Card)` 41 | flex: 1 1 auto; 42 | 43 | @media (max-width: ${CSS_VARS.MQ.SMALL}px) { 44 | max-width: 250px; 45 | } 46 | 47 | @media (max-width: ${CSS_VARS.MQ.MEDIUM}px) { 48 | width: 100%; 49 | flex: 1 0 auto; 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /webapp/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const IS_DEV = process.env.NODE_ENV === 'development'; 2 | export const DEVELOPMENT_DAY = '2019-12-12'; 3 | export const DATE_FORMAT = 'YYYY-MM-DD'; 4 | export const MONTH_DATE_FORMAT = 'YYYY-MM'; 5 | -------------------------------------------------------------------------------- /webapp/src/global-styles.tsx: -------------------------------------------------------------------------------- 1 | import { css, Global } from '@emotion/core'; 2 | import React from 'react'; 3 | 4 | export const COLORS = { 5 | CHART_BAR: '#4edfa5', 6 | DANGER: 'orangered', 7 | GRAY: '#f1f1f1', 8 | DARK_GRAY: '#626262', 9 | LIGHT_BLACK: '#404040', 10 | BLACK: '#323232', 11 | WHITE: '#fff', 12 | }; 13 | 14 | const styles = css` 15 | body, 16 | html, 17 | #root { 18 | font-size: 18px; 19 | background-color: ${COLORS.BLACK}; 20 | color: ${COLORS.WHITE}; 21 | font-family: Roboto, sans-serif, sans-serif, Verdana, Geneva, Tahoma; 22 | width: 100%; 23 | height: 100%; 24 | margin: 0; 25 | } 26 | 27 | h1, 28 | h2, 29 | h3, 30 | h4, 31 | h5, 32 | h6 { 33 | margin: 0; 34 | } 35 | 36 | ul { 37 | margin: 0; 38 | padding: 0; 39 | list-style: none; 40 | } 41 | 42 | button { 43 | border: 0; 44 | background: transparent; 45 | color: currentColor; 46 | font-size: 1em; 47 | cursor: pointer; 48 | } 49 | 50 | *, 51 | *::before, 52 | *::after { 53 | box-sizing: border-box; 54 | } 55 | `; 56 | 57 | export const GlobalStyles: React.SFC = () => ; 58 | -------------------------------------------------------------------------------- /webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | // fonts 6 | import 'typeface-roboto'; 7 | 8 | if (!window.requestIdleCallback) { 9 | window.requestIdleCallback = (cb: Function) => setTimeout(cb, 100); 10 | } 11 | 12 | ReactDOM.render(, document.getElementById('root')); 13 | -------------------------------------------------------------------------------- /webapp/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ITime { 2 | hours: number; 3 | minutes: number; 4 | } 5 | 6 | export interface IWorkDayTimetableRecord { 7 | day: string; 8 | date: string; 9 | events: string[]; 10 | homeArriveTime: string; 11 | homeLeaveTime: string; 12 | workArriveTime: string; 13 | workLeaveTime: string; 14 | } 15 | 16 | export interface IDayResult extends IWorkDayTimetableRecord { 17 | totalMorningCommuteTime: ITime; 18 | totalEveningCommuteTime: ITime; 19 | totalTimeAtOffice: ITime; 20 | } 21 | 22 | export interface ITimetableChartResult extends IWorkDayTimetableRecord { 23 | totalMorningCommuteTime: ITime; 24 | totalEveningCommuteTime: ITime; 25 | totalTimeAtOffice: ITime; 26 | } 27 | 28 | export interface IPeriodResult { 29 | totalTimeAtOffice: ITime; 30 | averageTimeCommuting: ITime; 31 | averageTimeAtOffice: ITime; 32 | timetableChart: ITimetableChartResult[]; 33 | } 34 | 35 | export interface IFirstRecordResult extends IWorkDayTimetableRecord {} 36 | -------------------------------------------------------------------------------- /webapp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "downlevelIteration": true 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | prettier@^1.19.1: 6 | version "1.19.1" 7 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" 8 | integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== 9 | --------------------------------------------------------------------------------