├── .firebaserc ├── .gitignore ├── .vscode └── launch.json ├── LICENSE.md ├── README.md ├── errorcodes.md ├── firebase.json ├── functions ├── index.js ├── package-lock.json ├── package.json └── yarn.lock ├── jsconfig.json ├── package.json ├── preact.config.js ├── src ├── assets │ ├── favicon.ico │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-256x256.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-18x18.png │ │ ├── favicon-32x32.png │ │ └── mstile-150x150.png │ ├── imgs │ │ ├── 404.svg │ │ ├── default_header.jpg │ │ ├── default_header.webp │ │ ├── default_profile_picture.png │ │ ├── login_background.jpg │ │ ├── mnged_class_screenshot.png │ │ ├── mnged_dashboard_and_class_screenshot_big.png │ │ ├── mnged_dashboard_screenshot.png │ │ ├── mnged_dashboard_screenshot_big.png │ │ ├── mnged_logo.png │ │ └── mnged_logo_small.png │ ├── lighthouse │ │ ├── lighthouse_accessibility.svg │ │ ├── lighthouse_best_practices.svg │ │ ├── lighthouse_performance.svg │ │ └── lighthouse_progressive_web_app.svg │ └── src │ │ └── firebase-messaging-sw.js ├── components │ ├── app.js │ ├── button │ │ ├── index.js │ │ └── style.scss │ ├── card │ │ ├── index.js │ │ └── style.scss │ ├── datePicker │ │ ├── index.js │ │ └── style.scss │ ├── dayNavigator │ │ ├── index.js │ │ └── style.scss │ ├── dialog │ │ ├── index.js │ │ └── style.scss │ ├── floatingActionButton │ │ ├── index.js │ │ └── style.scss │ ├── header │ │ ├── index.js │ │ └── style.scss │ ├── icons │ │ ├── index.js │ │ └── style.scss │ ├── loader │ │ ├── index.js │ │ └── style.scss │ ├── miniCard │ │ ├── index.js │ │ └── style.scss │ ├── nav │ │ ├── index.js │ │ └── style.scss │ ├── radioInput │ │ ├── index.js │ │ └── style.scss │ ├── selectInput │ │ ├── index.js │ │ └── style.scss │ ├── snackbar │ │ ├── index.js │ │ └── style.scss │ └── textInput │ │ ├── index.js │ │ └── style.scss ├── index.js ├── lib │ ├── firebase.js │ └── state │ │ ├── initializeState.js │ │ └── stores │ │ ├── .taskStore.js.swp │ │ ├── .userStore.js.swp │ │ ├── index.js │ │ ├── taskStore.js │ │ ├── uiStore.js │ │ └── userStore.js ├── manifest.json ├── routes │ ├── about │ │ ├── index.js │ │ └── style.scss │ ├── class │ │ ├── index.js │ │ └── style.scss │ ├── dashboard │ │ ├── classList │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── index.js │ │ ├── setClasses │ │ │ ├── index.js │ │ │ └── style.scss │ │ └── style.scss │ ├── donate │ │ ├── index.js │ │ └── style.scss │ ├── errorpage │ │ ├── index.js │ │ └── style.scss │ ├── home │ │ ├── addTask │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── index.js │ │ ├── style.scss │ │ └── taskItem │ │ │ ├── attachmentItem │ │ │ ├── index.js │ │ │ └── style.scss │ │ │ ├── index.js │ │ │ └── style.scss │ ├── index.js │ ├── register │ │ ├── index.js │ │ └── style.scss │ ├── schedule │ │ ├── index.js │ │ └── style.scss │ ├── settings │ │ ├── index.js │ │ └── style.scss │ ├── signin │ │ ├── index.js │ │ └── style.scss │ ├── task │ │ ├── index.js │ │ └── style.scss │ ├── tasks │ │ ├── addTask │ │ │ ├── index.js │ │ │ └── style.scss │ │ ├── index.js │ │ └── style.scss │ └── welcome │ │ ├── index.js │ │ ├── index.old.js │ │ └── style.scss ├── style │ ├── card.scss │ ├── index.scss │ └── vars.scss └── template.html └── yarn.lock /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "managed-me" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build 3 | /*.log 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceRoot}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marius Niveri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | MANAGED Logo 3 |

4 |
5 | ⚠ Attention: MNGED is not finished yet and there're many bugs present! ⚠
6 | MNGED is a PWA which helps you to keep track of your daily study life 🎓 7 |
8 |
9 | <coded/> with ❤︎ and ☕ by Marius Niveri

10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | ## Getting started 🚀 19 | To try it on your own run the following commands after you installed [yarn](https://yarnpkg.com/lang/en/): 20 | ```sh 21 | yarn 22 | yarn start 23 | ``` 24 | If you don't have [yarn](https://yarnpkg.com/lang/en/) installed, go ahead and do it. It's definitely worth it ;) 25 | If you have [nvm](https://github.com/creationix/nvm), you also should run `nvm use v8` to use node version 8.X.X 26 | ## Live Demo 🎉 27 | If you want to see how it currently looks like, you can go ahead and give it a visit via [https://mnged.me](https://mnged.me). This page is hosted on Firebase Hosting and should be updated. But please don't get driven crazy if something does not work. Everything is still under construction! 28 | ## About 29 | MNGED is a so called Progressive Web App (PWA). PWA's stand out because they are fast 🚀 and always work, even with no connection to the internet. 30 | ![Screenshot of the dashboard and a detailed view of a class](https://raw.githubusercontent.com/m4r1vs/mnged/master/src/assets/imgs/mnged_dashboard_and_class_screenshot_big.png) 31 | MNGED uses [Firebase 🔥](https://firebase.google.com) for authentication and as a database. Firebase is developed and maintained by Google. The authentication is build by the same team that also build the Google Sign In and is responsible for other security at Google. But that also means that Google has access to our database which you may or may not care about. 32 | 33 | As the UI provider I decided to go with [Preact](https://preactjs.com), a lightweight 3kb fork of React. For storing the state I use [MobX](https://mobx.js.org/getting-started.html), it's a simple but powerful state management solution. And finally as the database I went with Firebase, a mostly free hosting and database provided by Google. The nice thing about firebase is that it comes with a nice JavaScript library which enables Authentication and live-updates when the database changes. 34 | ## Contributers 😊 35 | Huge thanks to [Jason Miller](https://github.com/developit/) for building Preact and the Preact CLI. And also thanks to him for helping this projects to gain some popularity with his [Twitter Quote](https://twitter.com/_developit/status/923555370219470848)! -------------------------------------------------------------------------------- /errorcodes.md: -------------------------------------------------------------------------------- 1 | # Error codes 2 | This file stores a list of all possible error codes that will be thrown by the app when something goes wrong 3 | ### Firebase 4 | * #001: Firebase Firestore got a realtime update with a type of removed in the classes collection. Classes should never be removed, only modified. 5 | * #002: Firebase Firestore got a realtime update with no type specified. -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "rewrites": [ 4 | { 5 | "source": "/firebase-messaging-sw.js", 6 | "destination": "/assets/src/firebase-messaging-sw.js" 7 | }, 8 | { 9 | "source": "**", 10 | "destination": "/index.html" 11 | } 12 | ], 13 | "ignore": [ 14 | "firebase.json", 15 | "**/.*", 16 | "**/node_modules/**" 17 | ], 18 | "headers": [ 19 | { 20 | "source": "**", 21 | "headers": [ 22 | { 23 | "key": "Cache-Control", 24 | "value": "public, max-age=3600, no-cache" 25 | }, 26 | { 27 | "key": "Access-Control-Max-Age", 28 | "value": "600" 29 | } 30 | ] 31 | }, 32 | { 33 | "source": "/sw.js", 34 | "headers": [ 35 | { 36 | "key": "Cache-Control", 37 | "value": "private, no-cache" 38 | } 39 | ] 40 | }, 41 | { 42 | "source": "**/*.chunk.*.js", 43 | "headers": [ 44 | { 45 | "key": "Cache-Control", 46 | "value": "public, max-age=31536000" 47 | } 48 | ] 49 | }, 50 | { 51 | "source": "/", 52 | "headers": [ 53 | { 54 | "key": "Link", 55 | "value": "; rel=preload; as=script, ; rel=preload; as=style" 56 | } 57 | ] 58 | }, 59 | { 60 | "source": "/index", 61 | "headers": [ 62 | { 63 | "key": "Link", 64 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 65 | } 66 | ] 67 | }, 68 | { 69 | "source": "/dashboard", 70 | "headers": [ 71 | { 72 | "key": "Link", 73 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 74 | } 75 | ] 76 | }, 77 | { 78 | "source": "/tasks", 79 | "headers": [ 80 | { 81 | "key": "Link", 82 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 83 | } 84 | ] 85 | }, 86 | { 87 | "source": "/welcome", 88 | "headers": [ 89 | { 90 | "key": "Link", 91 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 92 | } 93 | ] 94 | }, 95 | { 96 | "source": "/task", 97 | "headers": [ 98 | { 99 | "key": "Link", 100 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 101 | } 102 | ] 103 | }, 104 | { 105 | "source": "/signin", 106 | "headers": [ 107 | { 108 | "key": "Link", 109 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 110 | } 111 | ] 112 | }, 113 | { 114 | "source": "/settings", 115 | "headers": [ 116 | { 117 | "key": "Link", 118 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 119 | } 120 | ] 121 | }, 122 | { 123 | "source": "/register", 124 | "headers": [ 125 | { 126 | "key": "Link", 127 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 128 | } 129 | ] 130 | }, 131 | { 132 | "source": "/errorpage", 133 | "headers": [ 134 | { 135 | "key": "Link", 136 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 137 | } 138 | ] 139 | }, 140 | { 141 | "source": "/class", 142 | "headers": [ 143 | { 144 | "key": "Link", 145 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 146 | } 147 | ] 148 | }, 149 | { 150 | "source": "/cafeteriaMenu", 151 | "headers": [ 152 | { 153 | "key": "Link", 154 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 155 | } 156 | ] 157 | }, 158 | { 159 | "source": "/about", 160 | "headers": [ 161 | { 162 | "key": "Link", 163 | "value": "; rel=preload; as=script, ; rel=preload; as=style, ; rel=preload; as=script" 164 | } 165 | ] 166 | } 167 | ], 168 | "public": "build" 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const admin = require('firebase-admin'); 2 | const request = require('request'); 3 | const functions = require('firebase-functions'); 4 | 5 | admin.initializeApp(functions.config().firebase); 6 | 7 | const firestore = admin.firestore(); 8 | 9 | const config = { 10 | googlePlusKey: 'AIzaSyBBuZrztM5uYu1b0pLZiRI2J60XoDZZvVo' 11 | }; 12 | 13 | const getHeader = user => { 14 | console.log('getHeader() started...'); 15 | let headerURL = null; 16 | 17 | if (typeof user.providerData !== 'object') return null; 18 | if (!user.providerData[0]) return null; 19 | if (user.providerData[0].providerId !== 'google.com') return null; 20 | 21 | const googleUid = user.providerData[0].uid; 22 | 23 | request('https://www.googleapis.com/plus/v1/people/' + googleUid + '?fields=cover%2FcoverPhoto%2Furl&key=' + config.googlePlusKey, (error, response, body) => { 24 | console.log('request done: ', response); 25 | if (!error && response.statusCode === 200) { 26 | const cover = JSON.parse(body).cover; 27 | console.log('Got cover: ', cover); 28 | if (!cover) return null; 29 | if (!cover.coverPhoto) return null; 30 | if (!cover.coverPhoto.url) return null; 31 | headerURL = cover.coverPhoto.url; 32 | } 33 | else console.error('Error fetching HeaderURL: ', error); 34 | }); 35 | 36 | return headerURL; 37 | }; 38 | 39 | exports.setUpNewUser = functions.auth.user().onCreate(event => { 40 | 41 | const user = event.data; 42 | 43 | const batch = firestore.batch(); 44 | const userRef = firestore.collection('user-data').doc(user.uid); 45 | 46 | batch.set(userRef, { 47 | name: user.displayName || 'Mnger', 48 | email: user.email || null, 49 | photoURL: user.photoURL || 'https://mnged.me/assets/imgs/default_profile_picture.png', 50 | headerURL: getHeader(user) || 'https://mnged.me/assets/imgs/default_header.jpg' 51 | }); 52 | 53 | const taskRef = userRef.collection('tasks').doc(); 54 | batch.set(taskRef, { 55 | title: 'My first Task: Explore MNGED!', 56 | due: new Date(new Date().getTime() + 604800000), 57 | created: new Date() 58 | }); 59 | 60 | batch.set(taskRef.collection('attachments').doc(), { 61 | type: 'note', 62 | title: 'A note by Marius', 63 | content: `Hi ${user.displayName || 'Mnger'}! I am Marius Niveri, the founder and creator of MNGED. I hope you enjoy using my apllication and also feel free to [contact me](https://twitter.com/mariusniveri) in case you have any questions or improvments regarding MNGED. I hope you have an awesome day!`, 64 | created: new Date() 65 | }); 66 | 67 | batch.commit() 68 | .then(() => console.log('Successfully created FireStore entry for user: ', user)) 69 | .catch(err => console.error('Error creating FireStore entry for user: ', err)); 70 | 71 | }); 72 | 73 | exports.onUserDelete = functions.auth.user().onDelete(event => { 74 | 75 | const user = event.data; 76 | 77 | firestore 78 | .collection('user-data') 79 | .doc(user.uid) 80 | .delete() 81 | .then(() => console.log('deleted FireStore entry for user: ', user)) 82 | .catch((err) => console.log('Something went wrong deleting FireStore entry for user: ', err)); 83 | }); 84 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "dependencies": { 5 | "firebase-admin": "^5.4.1", 6 | "firebase-functions": "^0.6.2", 7 | "request": "^2.83.0" 8 | }, 9 | "private": true 10 | } 11 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "experimentalDecorators": true 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "**/node_modules/*" 9 | ] 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mnged", 3 | "version": "0.3.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "if-env NODE_ENV=production && yarn run -s serve || yarn run -s dev", 8 | "build": "preact build --template src/template.html", 9 | "deploy": "yarn build && firebase deploy --only hosting", 10 | "serve": "preact build --template src/template.html && preact serve", 11 | "dev": "preact watch --template src/template.html -p 2211", 12 | "test": "eslint src && preact test" 13 | }, 14 | "eslintConfig": { 15 | "extends": "eslint-config-synacor" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^4.8.0", 19 | "eslint-config-synacor": "^2.0.2", 20 | "if-env": "^1.0.0", 21 | "node-sass": "^4.5.3", 22 | "preact-cli": "^1.4.1", 23 | "preact-cli-sw-precache": "^1.0.3", 24 | "sass-loader": "^6.0.6", 25 | "webpack-bundle-analyzer": "^2.9.0" 26 | }, 27 | "dependencies": { 28 | "firebase": "4.6.1", 29 | "mobx": "^3.3.1", 30 | "mobx-logger": "^0.6.0", 31 | "preact": "^8.2.5", 32 | "preact-compat": "^3.17.0", 33 | "preact-mobx": "^0.1.4", 34 | "preact-router": "^2.5.7", 35 | "xmlhttprequest": "^1.8.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /preact.config.js: -------------------------------------------------------------------------------- 1 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 2 | 3 | export default function (config, env, helpers) { 4 | config.plugins.push(new BundleAnalyzerPlugin()); 5 | config.node.fs = 'empty'; 6 | config.node.child_process = 'empty'; 7 | } -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/android-chrome-256x256.png -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-18x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/favicon-18x18.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/imgs/404.svg: -------------------------------------------------------------------------------- 1 | 404404 -------------------------------------------------------------------------------- /src/assets/imgs/default_header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/default_header.jpg -------------------------------------------------------------------------------- /src/assets/imgs/default_header.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/default_header.webp -------------------------------------------------------------------------------- /src/assets/imgs/default_profile_picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/default_profile_picture.png -------------------------------------------------------------------------------- /src/assets/imgs/login_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/login_background.jpg -------------------------------------------------------------------------------- /src/assets/imgs/mnged_class_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/mnged_class_screenshot.png -------------------------------------------------------------------------------- /src/assets/imgs/mnged_dashboard_and_class_screenshot_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/mnged_dashboard_and_class_screenshot_big.png -------------------------------------------------------------------------------- /src/assets/imgs/mnged_dashboard_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/mnged_dashboard_screenshot.png -------------------------------------------------------------------------------- /src/assets/imgs/mnged_dashboard_screenshot_big.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/mnged_dashboard_screenshot_big.png -------------------------------------------------------------------------------- /src/assets/imgs/mnged_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/mnged_logo.png -------------------------------------------------------------------------------- /src/assets/imgs/mnged_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/assets/imgs/mnged_logo_small.png -------------------------------------------------------------------------------- /src/assets/lighthouse/lighthouse_accessibility.svg: -------------------------------------------------------------------------------- 1 | lighthouse accessibilitylighthouse accessibility94%94% -------------------------------------------------------------------------------- /src/assets/lighthouse/lighthouse_best_practices.svg: -------------------------------------------------------------------------------- 1 | lighthouse best practiceslighthouse best practices94%94% -------------------------------------------------------------------------------- /src/assets/lighthouse/lighthouse_performance.svg: -------------------------------------------------------------------------------- 1 | lighthouse performancelighthouse performance86%86% -------------------------------------------------------------------------------- /src/assets/lighthouse/lighthouse_progressive_web_app.svg: -------------------------------------------------------------------------------- 1 | lighthouse progressive web applighthouse progressive web app91%91% -------------------------------------------------------------------------------- /src/assets/src/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('/__/firebase/3.9.0/firebase-app.js'); 2 | importScripts('/__/firebase/3.9.0/firebase-messaging.js'); 3 | importScripts('/__/firebase/init.js'); 4 | 5 | const messaging = firebase.messaging(); 6 | 7 | messaging.setBackgroundMessageHandler((payload) => { 8 | console.log('[firebase-messaging-sw.js] Received background message ', payload); 9 | // Customize notification here 10 | const notificationTitle = 'Background Message Title'; 11 | const notificationOptions = { 12 | body: 'Background Message body.', 13 | icon: '/firebase-logo.png' 14 | }; 15 | 16 | return self.registration.showNotification(notificationTitle, 17 | notificationOptions); 18 | }); -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | import { auth } from '../lib/firebase'; 4 | 5 | import Header from 'async!./header'; 6 | import Nav from 'async!./nav'; 7 | import SnackBar from './snackbar'; 8 | import Dialog from './dialog'; 9 | import DatePicker from './datePicker'; 10 | import Loader from './loader'; 11 | import Routes from 'async!../routes'; 12 | 13 | import initializeState from '../lib/state/initializeState'; 14 | 15 | @observer 16 | export default class App extends Component { 17 | 18 | componentWillMount() { 19 | 20 | // don't execute on prerender 21 | if (typeof window !== 'undefined') { 22 | 23 | // gets fired when user logs in/out and on initial load 24 | auth.onAuthStateChanged((user) => { 25 | 26 | // call function to set up listeners 27 | initializeState(this.props.stores, user); 28 | 29 | // if user logged on, show snackbar 30 | if (user) this.props.stores.uiStore.showSnackbar( 31 | 'Signed in as ' + user.email, 32 | null, 33 | 3500 34 | ); 35 | }); 36 | } 37 | } 38 | 39 | render({ stores }) { 40 | 41 | // If there's an error, render only the error 42 | if (stores.uiStore.error) { 43 | return ( 44 |
45 |

{stores.uiStore.error}

46 | 47 |
48 | ); 49 | } 50 | 51 | // assuming no error render the app: 52 | return ( 53 |
54 | 55 | 56 | 57 | 58 | 59 | {/* only show the header and nav drawer if in app mode */} 60 | {stores.userStore.user && ( 61 |
62 |
65 | )} 66 | 67 |
68 | {stores.uiStore.appLoaded ? : } 69 |
70 | 71 |
72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/button/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style'; 3 | 4 | export const DefaultButton = props => ( 5 | 8 | ); 9 | 10 | export const CancelButton = props => ( 11 | 14 | ); 15 | 16 | export const SubmitButton = props => ( 17 | 18 | ); 19 | 20 | export default DefaultButton; -------------------------------------------------------------------------------- /src/components/button/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .button { 4 | min-width: 120px; 5 | max-width: 140px; 6 | padding: 6px; 7 | border-radius: 2px; 8 | cursor: pointer; 9 | text-align: center; 10 | font-size: 16px; 11 | text-decoration: none; 12 | border: none; 13 | background-color: $secondary-color-dark; 14 | color: #fff; 15 | outline: none; 16 | transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); 17 | box-shadow: var(--button-box-shadow); 18 | 19 | &:active, &:hover { 20 | box-shadow: var(--button-box-shadow-hovered); 21 | } 22 | } 23 | 24 | .cancelButton { 25 | background-color: #ff5252; 26 | } -------------------------------------------------------------------------------- /src/components/card/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style'; 3 | 4 | const Card = props =>
{props.children}
; 5 | 6 | export default Card; -------------------------------------------------------------------------------- /src/components/card/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .card { 4 | padding: 0 16px; 5 | background: #fff; 6 | display: block; 7 | 8 | @media (min-width: 500px) { 9 | margin: 0 auto; 10 | border-radius: 3px; 11 | box-shadow: $card-box-shadow; 12 | max-width: 450px; 13 | } 14 | 15 | hr { 16 | border: none; 17 | margin: 0; 18 | margin-top: 15px; 19 | } 20 | } 21 | 22 | body[class=nightmode] { 23 | .card { 24 | background: #424242; 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/datePicker/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import style from './style'; 3 | import { observer } from 'preact-mobx'; 4 | 5 | @observer 6 | export default class DatePicker extends Component { 7 | 8 | closeDatePicker() { 9 | if (this.props.uiStore.datePicker) this.props.uiStore.datePicker.close(); // Function gets passed by parent 10 | } 11 | 12 | /** 13 | * Gets fired when a day gets clicked. 14 | * @param {object} e The event thrown by the element clicked 15 | */ 16 | dayClicked(e) { 17 | 18 | const element = e.target; // the actual element clicked 19 | 20 | if (element.innerHTML === '' || (element.getAttribute('disabled'))) return false; // don't continue if empty 21 | 22 | // get date from clicked element (gets attached when rendered) 23 | const date = new Date(element.getAttribute('date')); 24 | 25 | // update the state 26 | this.setState({ currentDate: date }); 27 | } 28 | 29 | /** 30 | * returns days in month as array 31 | * @param {number} month the month to display 32 | * @param {number} year the year to display 33 | */ 34 | getDaysByMonth(month, year) { 35 | 36 | let calendar = []; 37 | 38 | const firstDay = new Date(year, month, 1).getDay(); // first weekday of month 39 | const lastDate = new Date(year, month + 1, 0).getDate(); // last date of month 40 | 41 | let day = 0; 42 | 43 | // the calendar is 7*6 fields big, so 42 loops 44 | for (let i = 0; i < 42; i++) { 45 | 46 | if (i >= firstDay && day !== null) day = day + 1; 47 | if (day > lastDate) day = null; 48 | 49 | // append the calendar Array 50 | calendar.push({ 51 | day: (day === 0 || day === null) ? null : day, // null or number 52 | date: (day === 0 || day === null) ? null : new Date(year, month, day), // null or Date() 53 | disabled: this.props.onlyFutureDates ? ((day < this.now.getDate() || month < this.now.getMonth() || year < this.now.getFullYear()) && (month <= this.now.getMonth() && year <= this.now.getFullYear())) : false, 54 | today: (day === this.now.getDate() && month === this.now.getMonth() && year === this.now.getFullYear()) // boolean 55 | }); 56 | } 57 | 58 | return calendar; 59 | } 60 | 61 | /** 62 | * Display previous month by updating state 63 | */ 64 | displayPrevMonth() { 65 | if (this.state.displayedMonth <= 0) { 66 | this.setState({ 67 | displayedMonth: 11, 68 | displayedYear: this.state.displayedYear - 1 69 | }); 70 | } 71 | else { 72 | this.setState({ 73 | displayedMonth: this.state.displayedMonth - 1 74 | }); 75 | } 76 | } 77 | 78 | /** 79 | * Display next month by updating state 80 | */ 81 | displayNextMonth() { 82 | if (this.state.displayedMonth >= 11) { 83 | this.setState({ 84 | displayedMonth: 0, 85 | displayedYear: this.state.displayedYear + 1 86 | }); 87 | } 88 | else { 89 | this.setState({ 90 | displayedMonth: this.state.displayedMonth + 1 91 | }); 92 | } 93 | } 94 | 95 | /** 96 | * Display the selected month (gets fired when clicking on the date string) 97 | */ 98 | displaySelectedMonth() { 99 | if (this.state.selectYearMode) { 100 | this.toggleYearSelector(); 101 | } 102 | else { 103 | if (!this.state.currentDate) return false; 104 | this.setState({ 105 | displayedMonth: this.state.currentDate.getMonth(), 106 | displayedYear: this.state.currentDate.getFullYear() 107 | }); 108 | } 109 | } 110 | 111 | toggleYearSelector() { 112 | this.setState({ selectYearMode: !this.state.selectYearMode }); 113 | } 114 | 115 | changeDisplayedYear(e) { 116 | const element = e.target; 117 | this.toggleYearSelector(); 118 | this.setState({ displayedYear: parseInt(element.innerHTML, 10), displayedMonth: 0 }); 119 | } 120 | 121 | /** 122 | * Pass the selected date to parent when 'OK' is clicked 123 | */ 124 | passDateToParent() { 125 | if (this.props.uiStore.datePicker && typeof this.props.uiStore.datePicker.recieverFunction === 'function') { 126 | this.props.uiStore.datePicker.recieverFunction(this.state.currentDate); 127 | } 128 | this.closeDatePicker(); 129 | } 130 | 131 | constructor() { 132 | super(); 133 | 134 | this.closeDatePicker = this.closeDatePicker.bind(this); 135 | this.dayClicked = this.dayClicked.bind(this); 136 | this.displayNextMonth = this.displayNextMonth.bind(this); 137 | this.displayPrevMonth = this.displayPrevMonth.bind(this); 138 | this.getDaysByMonth = this.getDaysByMonth.bind(this); 139 | this.changeDisplayedYear = this.changeDisplayedYear.bind(this); 140 | this.passDateToParent = this.passDateToParent.bind(this); 141 | this.toggleYearSelector = this.toggleYearSelector.bind(this); 142 | this.displaySelectedMonth = this.displaySelectedMonth.bind(this); 143 | 144 | this.monthArrShortFull = [ 145 | 'January', 146 | 'February', 147 | 'March', 148 | 'April', 149 | 'May', 150 | 'June', 151 | 'July', 152 | 'August', 153 | 'September', 154 | 'October', 155 | 'November', 156 | 'December' 157 | ]; 158 | 159 | this.monthArrShort = [ 160 | 'Jan', 161 | 'Feb', 162 | 'Mar', 163 | 'Apr', 164 | 'May', 165 | 'Jun', 166 | 'Jul', 167 | 'Aug', 168 | 'Sep', 169 | 'Oct', 170 | 'Nov', 171 | 'Dec' 172 | ]; 173 | 174 | this.dayArr = [ 175 | 'Sun', 176 | 'Mon', 177 | 'Tue', 178 | 'Wed', 179 | 'Thu', 180 | 'Fri', 181 | 'Sat' 182 | ]; 183 | 184 | this.now = new Date(); 185 | 186 | this.yearArr = []; 187 | 188 | for (let i = 1970; i <= this.now.getFullYear() + 30; i++) { 189 | this.yearArr.push(i); 190 | } 191 | 192 | this.state = { 193 | currentDate: this.now, 194 | displayedMonth: this.now.getMonth(), 195 | displayedYear: this.now.getFullYear(), 196 | selectYearMode: false 197 | }; 198 | } 199 | 200 | componentDidUpdate() { 201 | if (this.state.selectYearMode) { 202 | document.getElementsByClassName(style.selectedYear)[0].scrollIntoView(); // works in every browser incl. IE, replace with scrollIntoViewIfNeeded when browsers support it 203 | } 204 | } 205 | 206 | render({ uiStore }) { 207 | 208 | const { currentDate, displayedMonth, displayedYear, selectYearMode } = this.state; 209 | const { opened } = uiStore.datePicker; 210 | 211 | return ( 212 |
213 |
214 | 215 |
216 |

{currentDate.getFullYear()}

220 |

224 | {this.dayArr[currentDate.getDay()]}, {this.monthArrShort[currentDate.getMonth()]} {currentDate.getDate()} 225 |

226 |
227 | 228 | {!selectYearMode && } 233 | 234 |
235 | 236 | {!selectYearMode &&
237 | 238 |
239 | {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => {day})} 240 |
241 | 242 |
243 | 244 | {/* 245 | Loop through the calendar object returned by getDaysByMonth(). 246 | */} 247 | 248 | {this.getDaysByMonth(this.state.displayedMonth, this.state.displayedYear) 249 | .map( 250 | day => { 251 | let selected = false; 252 | 253 | if (currentDate && day.date) selected = (currentDate.toLocaleDateString() === day.date.toLocaleDateString()); 254 | 255 | return ( 256 | 262 | {day.day} 263 | 264 | ); 265 | } 266 | ) 267 | } 268 | 269 |
270 | 271 |
} 272 | 273 | {selectYearMode &&
274 | 275 | {this.yearArr.map(year => ( 276 | 277 | {year} 278 | 279 | ))} 280 | 281 |
} 282 | 283 | {!selectYearMode &&
284 | 285 | 286 |
} 287 | 288 |
289 |
290 | 291 |
295 | 296 |
297 | ); 298 | } 299 | } -------------------------------------------------------------------------------- /src/components/datePicker/style.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | body, html { 6 | overflow-x: hidden; 7 | font-family: var(--font-stack); 8 | background-color: var(--primary-background-color); 9 | color: var(--primary-text-color-dark); 10 | margin: 0; 11 | padding: 0; 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | transition: background-color .22s; 16 | min-width: 100vw; 17 | min-height: 100vh; 18 | 19 | &.darkTheme { 20 | --primary-text-color-dark: rgba(255,255,255,.87); 21 | --secondary-text-color-dark: rgba(255,255,255,.57); 22 | --disabled-text-color-dark: rgba(255,255,255,.13); 23 | 24 | --primary-text-color-light: rgba(0,0,0,.87); 25 | --secondary-text-color-light: rgba(0,0,0,.57); 26 | --disabled-text-color-light: rgba(0,0,0,.13); 27 | 28 | --primary-card-color: #424242; 29 | --primary-background-color: #303030; 30 | } 31 | } 32 | 33 | body { 34 | padding-top: 56px; 35 | } 36 | 37 | @keyframes fadeIn { 38 | from { 39 | opacity: 0; 40 | } 41 | to { 42 | opacity: 1; 43 | } 44 | } 45 | 46 | a { 47 | text-decoration: none; 48 | font-weight: 700; 49 | color: inherit; 50 | 51 | &:hover, &:focus { 52 | color: var(--secondary-color-dark); 53 | outline: none; 54 | text-decoration: underline; 55 | } 56 | } 57 | 58 | main, article { 59 | transition: background-color .22s; 60 | background-color: var(--primary-card-color); 61 | width: calc(100% - 32px); 62 | height: auto; 63 | box-shadow: var(--box-shadow-lvl-1); 64 | margin: 16px auto; 65 | max-width: 700px; 66 | padding: 8px 16px; 67 | border-radius: 3px; 68 | text-align: center; 69 | 70 | h1 { 71 | line-height: 32px; 72 | font-size: 28px; 73 | margin: 0; 74 | padding: 16px 0; 75 | } 76 | 77 | p { 78 | text-align: justify; 79 | line-height: 1.5; 80 | margin: 0; 81 | margin-bottom: 16px; 82 | } 83 | 84 | .infoBox { 85 | background: var(--primary-color); 86 | color: rgba(255,255,255,.87); 87 | padding: 8px 16px; 88 | display: inline-block; 89 | margin-bottom: 16px; 90 | border-radius: 3px; 91 | 92 | &.red { 93 | background: #ff8a80; 94 | color: rgba(0,0,0,.87); 95 | } 96 | } 97 | 98 | button { 99 | cursor: pointer; 100 | appearance: none; 101 | background: transparent; 102 | border: none; 103 | font-size: 16px; 104 | line-height: 1; 105 | padding: 8px 16px; 106 | border-radius: 3px; 107 | color: var(--primary-text-color); 108 | transition: background .13s; 109 | margin-bottom: 16px; 110 | 111 | &:hover, &:focus { 112 | outline: none; 113 | background: var(--disabled-text-color-dark); 114 | } 115 | } 116 | } 117 | 118 | article { 119 | margin-bottom: 56px; 120 | } 121 | 122 | footer { 123 | position: absolute; 124 | bottom: 16px; 125 | width: 100%; 126 | text-align: center; 127 | } 128 | 129 | .datePicker { 130 | text-align: left; 131 | background: var(--primary-card-color); 132 | border-radius: 3px; 133 | z-index: 200; 134 | position: fixed; 135 | height: auto; 136 | max-height: 90vh; 137 | width: 90vw; 138 | max-width: 448px; 139 | transform-origin: top left; 140 | transition: transform .22s ease-in-out, opacity .22s ease-in-out; 141 | top: 50%; 142 | left: 50%; 143 | opacity: 0; 144 | transform: scale(0) translate(-50%, -50%); 145 | user-select: none; 146 | 147 | &.opened { 148 | opacity: 1; 149 | transform: scale(1) translate(-50%, -50%); 150 | } 151 | 152 | .titles { 153 | border-top-left-radius: 3px; 154 | border-top-right-radius: 3px; 155 | padding: 24px; 156 | height: 100px; 157 | background: var(--primary-color); 158 | 159 | h2, h3 { 160 | cursor: pointer; 161 | color: #fff; 162 | line-height: 1; 163 | padding: 0; 164 | margin: 0; 165 | font-size: 32px; 166 | } 167 | 168 | h3 { 169 | color: rgba(255,255,255,.57); 170 | font-size: 18px; 171 | padding-bottom: 2px; 172 | } 173 | } 174 | 175 | nav { 176 | padding: 20px; 177 | height: 56px; 178 | 179 | h4 { 180 | width: calc(100% - 60px); 181 | text-align: center; 182 | display: inline-block; 183 | padding: 0; 184 | font-size: 14px; 185 | line-height: 24px; 186 | margin: 0; 187 | position: relative; 188 | top: -9px; 189 | color: var(--primary-text-color); 190 | } 191 | 192 | i { 193 | cursor: pointer; 194 | color: var(--secondary-text-color); 195 | font-size: 26px; 196 | user-select: none; 197 | border-radius: 50%; 198 | 199 | &:hover { 200 | background: var(--disabled-text-color-dark); 201 | } 202 | } 203 | } 204 | 205 | .scroll { 206 | overflow-y: auto; 207 | max-height: calc(90vh - 56px - 100px); 208 | } 209 | 210 | .calendar { 211 | padding: 0 20px; 212 | 213 | .dayNames { 214 | width: 100%; 215 | display: grid; 216 | text-align: center; 217 | 218 | // there's probably a better way to do this, but wanted to try out CSS grid 219 | grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7); 220 | 221 | span { 222 | color: var(--secondary-text-color-dark); 223 | font-size: 14px; 224 | line-height: 42px; 225 | display: inline-grid; 226 | } 227 | } 228 | 229 | .days { 230 | width: 100%; 231 | display: grid; 232 | text-align: center; 233 | grid-template-columns: calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7) calc(100% / 7); 234 | 235 | span { 236 | color: var(--primary-text-color-dark); 237 | line-height: 42px; 238 | font-size: 14px; 239 | display: inline-grid; 240 | transition: color .22s; 241 | height: 42px; 242 | position: relative; 243 | cursor: pointer; 244 | user-select: none; 245 | border-radius: 50%; 246 | 247 | &::before { 248 | content: ''; 249 | position: absolute; 250 | z-index: -1; 251 | height: 42px; 252 | width: 42px; 253 | left: calc(50% - 21px); 254 | background: var(--primary-color); 255 | border-radius: 50%; 256 | transition: transform .22s, opacity .22s; 257 | transform: scale(0); 258 | opacity: 0; 259 | } 260 | 261 | &[displayed=false] { 262 | cursor: unset; 263 | } 264 | 265 | &[disabled=true] { 266 | color: var(--disabled-text-color-dark); 267 | } 268 | 269 | &.today { 270 | font-weight: 700; 271 | } 272 | 273 | &.selected { 274 | color: rgba(255,255,255,.87); 275 | 276 | &:before { 277 | transform: scale(1); 278 | opacity: 1; 279 | } 280 | } 281 | } 282 | } 283 | } 284 | 285 | .selectYear { 286 | padding: 0 20px; 287 | display: block; 288 | width: 100%; 289 | text-align: center; 290 | max-height: 362px; 291 | 292 | span { 293 | display: block; 294 | width: 100%; 295 | font-size: 24px; 296 | margin: 20px auto; 297 | cursor: pointer; 298 | 299 | &.selectedYear { 300 | font-size: 42px; 301 | color: var(--primary-color); 302 | } 303 | } 304 | } 305 | 306 | div.actions { 307 | width: 100%; 308 | padding: 8px; 309 | text-align: right; 310 | 311 | button { 312 | margin-bottom: 0; 313 | font-size: 15px; 314 | cursor: pointer; 315 | color: var(--primary-text-color); 316 | border: none; 317 | margin-left: 8px; 318 | min-width: 64px; 319 | line-height: 36px; 320 | background-color: transparent; 321 | appearance: none; 322 | padding: 0 16px; 323 | border-radius: 3px; 324 | transition: background-color .13s; 325 | 326 | &:hover, &:focus { 327 | outline: none; 328 | background-color: var(--disabled-text-color-dark); 329 | } 330 | } 331 | } 332 | } 333 | 334 | .background { 335 | z-index: 199; 336 | position: fixed; 337 | top: 0; 338 | left: 0; 339 | bottom: 0; 340 | right: 0; 341 | background: rgba(0,0,0,.52); 342 | animation: fadeIn .22s forwards; 343 | } -------------------------------------------------------------------------------- /src/components/dayNavigator/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | import style from './style'; 4 | 5 | export default class DayNavigator extends Component { 6 | 7 | componentDidMount() { 8 | this.props.stores.uiStore.toggleDayNav(); 9 | } 10 | 11 | componentWillUnmount() { 12 | this.props.stores.uiStore.toggleDayNav(); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 21 |

22 | {this.props.title} 23 |

24 | 27 |
28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /src/components/dayNavigator/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .dayNavigator { 4 | background: #fff; 5 | width: 100%; 6 | height: 56px; 7 | border-radius: 0; 8 | box-shadow: $bottom-nav-box-shadow; 9 | position: fixed; 10 | bottom: 0; 11 | left: 0; 12 | z-index: 10; 13 | 14 | @media only screen and (min-width: 700px) { 15 | box-shadow: $card-box-shadow; 16 | background: $secondary-color-light; 17 | position: relative; 18 | border-radius: 3px; 19 | margin-top: 8px; 20 | margin-bottom: 16px; 21 | top: 0; 22 | bottom: auto; 23 | } 24 | 25 | button { 26 | outline: none; 27 | background: none; 28 | appearance: none; 29 | border: none; 30 | cursor: pointer; 31 | display: inline-block; 32 | 33 | &.left { 34 | float: left; 35 | } 36 | 37 | &.right { 38 | float: right; 39 | } 40 | 41 | &:hover { 42 | background: rgba(0, 0, 0, .05); 43 | } 44 | 45 | i { 46 | line-height: 56px; 47 | font-size: 2rem; 48 | } 49 | } 50 | 51 | h2 { 52 | position: relative; 53 | text-align: center; 54 | display: inline-block; 55 | font-size: 1.2rem; 56 | width: calc(100% - 88px); 57 | } 58 | } 59 | 60 | div[class=top] { 61 | display: none; 62 | } 63 | 64 | body[class=nightmode] { 65 | .dayNavigator { 66 | background: $secondary-color-dark; 67 | 68 | h2, i { 69 | color: #fff; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/components/dialog/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | 4 | import style from './style'; 5 | 6 | @observer 7 | export default class Dialog extends Component { 8 | 9 | closeDialog() { 10 | this.props.uiStore.closeDialog(); 11 | } 12 | 13 | constructor() { 14 | super(); 15 | this.closeDialog = this.closeDialog.bind(this); 16 | } 17 | 18 | render({ uiStore, children }) { 19 | 20 | const { dialog } = uiStore; 21 | 22 | return ( 23 |
24 |
25 |

{dialog && dialog.title}

30 | {dialog && dialog.content} 31 |
32 | 33 | 34 |
35 |
36 |
40 |
41 | ); 42 | } 43 | } -------------------------------------------------------------------------------- /src/components/dialog/style.scss: -------------------------------------------------------------------------------- 1 | .dialog { 2 | z-index: 200; 3 | position: fixed; 4 | height: auto; 5 | max-height: 512px; 6 | width: 90vw; 7 | max-width: 600px; 8 | transform-origin: bottom left; 9 | transition: transform .22s ease-in-out, opacity .22s ease-in-out; 10 | top: 40%; 11 | left: 50%; 12 | opacity: 0; 13 | transform: scale(0) translate(-50%, -50%); 14 | 15 | &.opened { 16 | opacity: 1; 17 | transform: scale(1) translate(-50%, -50%); 18 | } 19 | 20 | p, ul, ol { 21 | color: var(--primary-text-color); 22 | } 23 | 24 | h2 { 25 | color: var(--primary-text-color); 26 | line-height: 1; 27 | padding: 24px; 28 | } 29 | 30 | p { 31 | } 32 | 33 | ul, ol { 34 | 35 | li { 36 | 37 | } 38 | } 39 | 40 | form { 41 | margin: 0 24px 24px 24px; 42 | } 43 | 44 | div.actions { 45 | height: 52px; 46 | width: 100%; 47 | padding: 8px; 48 | text-align: right; 49 | 50 | button { 51 | cursor: pointer; 52 | color: var(--primary-color); 53 | border: none; 54 | margin-left: 8px; 55 | min-width: 64px; 56 | line-height: 36px; 57 | background-color: transparent; 58 | appearance: none; 59 | padding: 0 16px; 60 | border-radius: 3px; 61 | transition: background-color .13s; 62 | 63 | &:hover { 64 | outline: none; 65 | background-color: rgba(0,0,0,.07); 66 | } 67 | 68 | &:focus { 69 | outline: none; 70 | background-color: rgba(0,0,0,.13); 71 | } 72 | } 73 | } 74 | } 75 | 76 | @keyframes fadeIn { 77 | from { 78 | opacity: 0; 79 | } 80 | to { 81 | opacity: 1; 82 | } 83 | } 84 | 85 | .background { 86 | z-index: 199; 87 | position: fixed; 88 | top: 0; 89 | left: 0; 90 | bottom: 0; 91 | right: 0; 92 | background: rgba(0,0,0,.52); 93 | animation: fadeIn .22s forwards; 94 | } -------------------------------------------------------------------------------- /src/components/floatingActionButton/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import style from './style'; 3 | 4 | export default class FloatingActionButton extends Component { 5 | 6 | render() { 7 | return ( 8 |
this.fab = div} id="floatingActionButton" class={style.fab} {...this.props}> 9 | {this.props.children} 10 |
11 | ); 12 | } 13 | } -------------------------------------------------------------------------------- /src/components/floatingActionButton/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .fab { 4 | background-color: $secondary-color; 5 | position: fixed; 6 | height: 56px; 7 | cursor: pointer; 8 | width: 56px; 9 | border-radius: 50%; 10 | bottom: 22px; 11 | right: 22px; 12 | z-index: 10; 13 | overflow: hidden; 14 | box-shadow: 0 3px 6px rgba(0, 0, 0, .3); 15 | transition: bottom .22s; 16 | 17 | i { 18 | font-size: 24px; 19 | color: $primary-text-color; 20 | position: relative; 21 | top: 16px; 22 | left: 16px; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | import style from './style'; 4 | 5 | @observer 6 | export default class Header extends Component { 7 | 8 | openDrawer(e) { 9 | if (typeof window !== 'undefined') { 10 | if (e.path[0].id === 'navbtn-arrow' || e.path[1].id === 'navbtn-arrow') window.history.back(); 11 | else { 12 | document.body.style.overflow = 'hidden'; 13 | 14 | const drawer = document.getElementById('drawer'); 15 | const greyback = document.getElementById('drawerback'); 16 | 17 | greyback.style.display = 'block'; 18 | 19 | drawer.style.transition = 'margin-left .16s cubic-bezier(0.0, 0.0, 0.2, 1)'; 20 | greyback.style.transition = 'opacity .16s linear'; 21 | 22 | drawer.style.opacity = '1'; 23 | greyback.style.opacity = '1'; 24 | 25 | drawer.style.marginLeft = '0px'; 26 | } 27 | } 28 | } 29 | 30 | scrollToTop() { 31 | let scrollDuration = 512, 32 | cosParameter = document.body.scrollTop / 2, 33 | scrollCount = 0, 34 | oldTimestamp = performance.now(); 35 | 36 | const step = newTimestamp => { 37 | scrollCount += Math.PI / (scrollDuration / (newTimestamp - oldTimestamp)); 38 | if (scrollCount >= Math.PI) document.body.scrollTo(0, 0); 39 | if (document.body.scrollTop === 0) return; 40 | document.body.scrollTo(0, Math.round(cosParameter + cosParameter * Math.cos(scrollCount))); 41 | oldTimestamp = newTimestamp; 42 | window.requestAnimationFrame(step); 43 | }; 44 | 45 | window.requestAnimationFrame(step); 46 | } 47 | 48 | componentDidMount() { 49 | if (this.props.nightmode === true) document.body.classList.add('nightmode'); 50 | } 51 | 52 | render({ stores }) { 53 | 54 | const { subPage } = stores.uiStore; 55 | 56 | return ( 57 |
58 | 59 |
60 | 61 | 62 | 63 |
64 | 65 | school 66 | 67 |

{subPage ? subPage.headerTitle : 'Managed Me!'}

68 | 69 | {subPage && subPage.headerAction()} class={'material-icons ' + style.actionsIcon}>{subPage.headerActionIcon}} 70 | 71 |
72 | ); 73 | } 74 | } -------------------------------------------------------------------------------- /src/components/header/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .header { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | width: 100%; 8 | height: 56px; 9 | padding: 16px; 10 | background: $primary-color; 11 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); 12 | z-index: 50; 13 | transition: height .22s, top .05s ease-in-out, background-color .22s; 14 | 15 | @media (max-width: 1024px) { 16 | &[id=header-big] { 17 | height: 132px; 18 | top: 0 !important; 19 | h1 { 20 | margin-left: 0; 21 | padding-left: 0; 22 | bottom: 16px; 23 | font-size: 28px; 24 | } 25 | 26 | i { 27 | display: block; 28 | } 29 | } 30 | } 31 | 32 | &.hidden { 33 | top: -56px; 34 | } 35 | 36 | i { 37 | color: #fff; 38 | } 39 | 40 | i.actionsIcon { 41 | float: right; 42 | display: none; 43 | } 44 | 45 | i.mngedIcon { 46 | position: absolute; 47 | margin-left: 4px; 48 | 49 | @media (max-width: 1024px) { 50 | display: none; 51 | } 52 | } 53 | 54 | // svg { 55 | // height: 24px; 56 | // fill: #fff; 57 | // padding: 0 15px; 58 | // margin: 0 0 0 30px; 59 | 60 | // @media (min-width: 1025px) { 61 | // padding: 0 !important; 62 | // margin: 0 0 0 8px; 63 | // } 64 | // } 65 | 66 | .navbtn { 67 | user-select: none; 68 | width: 22px; 69 | height: 18px; 70 | position: absolute; 71 | margin: 3px auto; 72 | cursor: pointer; 73 | transition: all 0.2s ease-in-out; 74 | 75 | @media (min-width: 1025px) { 76 | opacity: 0; 77 | cursor: unset; 78 | margin-left: 8px !important; 79 | } 80 | 81 | span { 82 | display: block; 83 | position: absolute; 84 | height: 2px; 85 | width: 100%; 86 | background: #fff; 87 | left: 0; 88 | transition: 0.2s ease-in-out; 89 | 90 | &:nth-child(1) { 91 | top: 2px; 92 | } 93 | 94 | &:nth-child(2) { 95 | top: 8px; 96 | } 97 | 98 | &:nth-child(3) { 99 | top: 14px; 100 | } 101 | } 102 | 103 | &[id=navbtn-arrow] { 104 | transform: rotate(180deg) !important; 105 | 106 | @media (min-width: 1025px) { 107 | opacity: 1 !important; 108 | } 109 | 110 | span { 111 | 112 | &:nth-child(1) { 113 | transform: rotate(45deg); 114 | width: 50%; 115 | left: 12px; 116 | top: 4px; 117 | } 118 | 119 | &:nth-child(3) { 120 | transform: rotate(-45deg); 121 | width: 50%; 122 | left: 12px; 123 | top: 12px; 124 | } 125 | } 126 | } 127 | } 128 | 129 | h1 { 130 | text-decoration: none; 131 | color: #fff; 132 | font-size: 20px; 133 | padding: 0 15px; 134 | margin: 0 0 0 30px; 135 | position: absolute; 136 | font-weight: 400; 137 | transition: all .22s; 138 | 139 | @media (min-width: 1025px) { 140 | padding: 0 !important; 141 | margin: 0 0 0 59px; 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/components/icons/style.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | height: 24px; 3 | position: relative; 4 | top: 6px; 5 | margin-right: 30px; 6 | 7 | path[class=applyHoverEffect], circle[class=applyHoverEffect], rect[class=applyHoverEffect] { 8 | fill: transparent; 9 | transition: fill, .13s; 10 | } 11 | 12 | &:hover, &:focus { 13 | path[class=applyHoverEffect], circle[class=applyHoverEffect], rect[class=applyHoverEffect] { 14 | fill: var(--hover-color); 15 | } 16 | } 17 | } 18 | 19 | a:hover, a[class=active] { 20 | .icon { 21 | path[class=applyHoverEffect], circle[class=applyHoverEffect], rect[class=applyHoverEffect] { 22 | fill: var(--hover-color); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/loader/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | import style from './style'; 4 | 5 | const Loader = () => ( 6 |
7 | 8 | 9 | 10 | 13 | 16 | 17 | 18 | 21 | 24 | 25 | 26 | 27 |
28 | ); 29 | 30 | export default Loader; -------------------------------------------------------------------------------- /src/components/loader/style.scss: -------------------------------------------------------------------------------- 1 | $offset: 187; 2 | $duration: 1.4s; 3 | 4 | .loader { 5 | margin-top: calc( 50vh - 65px ); 6 | } -------------------------------------------------------------------------------- /src/components/miniCard/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style'; 3 | 4 | const MiniCard = props => ( 5 |
Hi
6 | ); 7 | 8 | export default MiniCard; -------------------------------------------------------------------------------- /src/components/miniCard/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/components/miniCard/style.scss -------------------------------------------------------------------------------- /src/components/nav/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | import { route } from 'preact-router'; 4 | import { auth } from '../../lib/firebase'; 5 | 6 | import { TasksIcon, ProjectsIcon, SettingsIcon, FeedbackIcon, AboutIcon, LogoutIcon, DonationIcon } from '../icons'; 7 | 8 | import { observer } from 'preact-mobx'; 9 | import style from './style'; 10 | 11 | @observer 12 | export default class Nav extends Component { 13 | 14 | toggleMore() { 15 | this.setState({ moreOpened: !this.state.moreOpened }); 16 | } 17 | 18 | closeDrawer() { 19 | if (typeof window === 'object') { 20 | window.closeDrawer(); 21 | } 22 | } 23 | 24 | constructor() { 25 | super(); 26 | 27 | this.state = { 28 | moreOpened: false 29 | }; 30 | } 31 | 32 | componentDidMount() { 33 | if (typeof window === 'object') { 34 | 35 | document.querySelectorAll('.applyHoverEffect').forEach((element) => { 36 | const hoverColor = element.getAttribute('fill'); // get the fill color 37 | 38 | // set it as a custom property inline 39 | if (hoverColor) element.style.setProperty('--hover-color', hoverColor); 40 | }); 41 | 42 | const panel = document.getElementById('main_component'); 43 | const drawer = this.drawer; 44 | const greyback = document.getElementById('drawerback'); 45 | 46 | let drawerwidth = drawer.offsetWidth; 47 | let startx = 0; 48 | let distgrey = 0; 49 | let open = false; 50 | let opened = false; 51 | let greybackstartx = 0; 52 | 53 | const drawerTransition = (state, bezier) => { 54 | if (state) { 55 | if (bezier === 'in') { 56 | drawer.style.transition = 'margin-left .225s cubic-bezier(0.0, 0.0, 0.2, 1), opacity .225s cubic-bezier(1,0,1,0)'; 57 | } 58 | else if (bezier === 'out') { 59 | drawer.style.transition = 'margin-left .195s cubic-bezier(0.4, 0.0, 0.6, 1), opacity .195s cubic-bezier(1,0,1,0)'; 60 | } 61 | greyback.style.transition = 'opacity .225s linear'; 62 | } 63 | else { 64 | drawer.style.transition = 'none'; 65 | greyback.style.transition = 'none'; 66 | } 67 | }; 68 | 69 | const drawerClosing = () => { 70 | document.body.style.overflow = 'auto'; 71 | opened = false; 72 | drawer.style.marginLeft = '-' + drawerwidth + 'px'; 73 | drawer.style.opacity = '0'; 74 | greyback.style.opacity = '0'; 75 | setTimeout(() => { 76 | greyback.style.display = 'none'; 77 | }, 225); 78 | }; 79 | 80 | const drawerOpening = () => { 81 | document.body.style.overflow = 'hidden'; 82 | opened = true; 83 | drawer.style.marginLeft = '0px'; 84 | drawer.style.opacity = '1'; 85 | greyback.style.opacity = '1'; 86 | greyback.style.display = 'block'; 87 | }; 88 | 89 | window.closeDrawer = bool => { 90 | if (!bool) { 91 | drawerTransition(true, 'out'); 92 | } 93 | else { 94 | drawerTransition(false, false); 95 | } 96 | drawerwidth = drawer.offsetWidth; 97 | drawerClosing(); 98 | }; 99 | 100 | window.addEventListener('resize', (e) => { 101 | window.closeDrawer(true); 102 | }); 103 | 104 | panel.addEventListener('touchstart', (e) => { 105 | let touchobj = e.changedTouches[0]; 106 | 107 | drawerTransition(false, false); 108 | 109 | startx = parseInt(touchobj.clientX, 10); 110 | if (startx < 25 && document.getElementById('navbtn')) { 111 | this.setState({ opened: true }); 112 | drawer.style.opacity = '1'; 113 | greyback.style.opacity = '0'; 114 | greyback.style.display = 'block'; 115 | open = true; 116 | } 117 | else { 118 | open = false; 119 | } 120 | }, { 121 | passive: true 122 | }); 123 | 124 | panel.addEventListener('touchmove', (e) => { 125 | let touchobj = e.changedTouches[0]; 126 | let dist = parseInt(touchobj.clientX, 10) - startx; 127 | if (open) { 128 | document.body.style.overflow = 'hidden'; 129 | drawerwidth = drawer.offsetWidth; 130 | 131 | if (dist <= drawerwidth) { 132 | drawer.style.marginLeft = dist - drawerwidth + 'px'; 133 | greyback.style.opacity = dist / drawerwidth; 134 | } 135 | } 136 | }, { 137 | passive: true 138 | }); 139 | 140 | panel.addEventListener('touchend', (e) => { 141 | drawerTransition(true, 'in'); 142 | let touchobj = e.changedTouches[0]; // Der erste Finger der den Bildschirm berührt wird gezählt 143 | if (open) { 144 | if (touchobj.clientX > 95) { 145 | greyback.style.opacity = '1'; 146 | drawer.style.marginLeft = '0px'; 147 | } 148 | else { 149 | drawerClosing(); 150 | } 151 | } 152 | }, { 153 | passive: true 154 | }); 155 | 156 | greyback.addEventListener('touchstart', (e) => { 157 | drawerTransition(false, false); 158 | let touchobj = e.changedTouches[0]; // Der erste Finger der den Bildschirm berührt wird gezählt 159 | greybackstartx = parseInt(touchobj.clientX, 10); 160 | }, { 161 | passive: true 162 | }); 163 | 164 | greyback.addEventListener('touchmove', (e) => { 165 | let touchobj = e.changedTouches[0]; 166 | distgrey = parseInt(touchobj.clientX, 10) - greybackstartx; 167 | if (distgrey < 0) { 168 | drawerwidth = drawer.offsetWidth; 169 | 170 | drawer.style.marginLeft = distgrey + 'px'; 171 | greyback.style.opacity = 1 - (Math.abs(distgrey / drawerwidth)); 172 | } 173 | }, { 174 | passive: true 175 | }); 176 | 177 | greyback.addEventListener('touchend', () => { 178 | drawerwidth = drawer.offsetWidth; 179 | 180 | if (distgrey > -80) { 181 | drawerTransition(true, 'in'); 182 | drawerOpening(); 183 | } 184 | else { 185 | drawerTransition(true, 'out'); 186 | drawerClosing(); 187 | } 188 | }, { 189 | passive: true 190 | }); 191 | } 192 | } 193 | 194 | render() { 195 | 196 | const { user } = this.props.stores.userStore; 197 | const profilePic = user ? user.photoURL : '/assets/imgs/default_profile_picture.png'; 198 | const headerURL = user ? user.headerURL : '/assets/imgs/default_header.jpg'; 199 | 200 | const moreOpened = this.state.moreOpened; 201 | const toggleMoreVar = { 202 | className: moreOpened ? style.drawerHeaderMore : '' 203 | }; 204 | 205 | const signOut = () => { 206 | this.closeDrawer(); 207 | auth.signOut().then(() => { 208 | this.props.stores.uiStore.showSnackbar( 209 | 'Signed out successfully', 210 | null, 211 | 5000 212 | ); 213 | }).catch((e) => { 214 | console.error(e); 215 | this.props.stores.uiStore.showSnackbar( 216 | 'An error occured during sign out', 217 | 'REPORT', 218 | 10000, 219 | () => route('/feedback') 220 | ); 221 | }); 222 | }; 223 | 224 | const styles = { 225 | profilePic: { 226 | backgroundImage: 'url(' + profilePic + ')' 227 | }, 228 | headerPic: { 229 | backgroundImage: 'url(' + headerURL + ')' 230 | } 231 | }; 232 | 233 | return ( 234 |
235 | 272 |
273 |
274 | ); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/components/nav/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .nav { 4 | background: #fff; 5 | width: 95%; // Fallback 6 | width: calc(100vw - 56px); 7 | position: fixed; 8 | margin: 0 0 0 -360px; 9 | box-shadow: 0 0 16px rgba(0, 0, 0, .7); 10 | max-width: 320px; 11 | height: 100%; 12 | height: 100vh; 13 | overflow-x: hidden; 14 | overflow-y: auto; 15 | z-index: 100; 16 | top: 0; 17 | transition: none; 18 | 19 | @media (min-width: 1025px) { 20 | width: 210px !important; 21 | display: block !important; 22 | opacity: 1 !important; 23 | margin-left: 8px !important; 24 | top: 64px !important; 25 | background: transparent !important; 26 | box-shadow: none !important; 27 | transition: none !important; 28 | } 29 | 30 | div.drawerHeader { 31 | border-bottom: 1px solid #000; 32 | height: 146px; 33 | width: 100%; 34 | background-size: cover; 35 | background-position: center; 36 | overflow: hidden; 37 | display: block; 38 | margin-bottom: 8px; 39 | 40 | @media (min-width: 1025px) { 41 | display: none !important; 42 | } 43 | 44 | div { 45 | background-color: rgba(0, 0, 0, .22); 46 | height: 100%; 47 | height: calc(100% + 1px); 48 | width: 100%; 49 | 50 | div.drawerHeaderProfilePic { 51 | border-radius: 50%; 52 | position: relative; 53 | height: 64px; 54 | width: 64px; 55 | background-size: cover; 56 | left: 16px; 57 | top: 16px; 58 | } 59 | 60 | span.drawerHeaderName { 61 | font-size: 14px; 62 | color: rgba(255, 255, 255, 1); 63 | position: absolute; 64 | top: calc(138px - 32px); 65 | left: 16px; 66 | } 67 | 68 | span.drawerHeaderMail { 69 | font-size: 14px; 70 | color: rgba(255, 255, 255, .74); 71 | position: absolute; 72 | top: calc(138px - 16px); 73 | left: 16px; 74 | } 75 | 76 | i { 77 | color: #fff; 78 | position: absolute; 79 | top: calc(138px - 28px); 80 | right: 16px; 81 | transition: .2s ease-in-out; 82 | cursor: pointer; 83 | user-select: none; 84 | } 85 | 86 | i.drawerHeaderMore { 87 | transform: rotate(180deg); 88 | } 89 | } 90 | } 91 | 92 | div.drawerContent { 93 | 94 | height: calc(100% - 155px); 95 | width: 100%; 96 | 97 | hr { 98 | display: none; 99 | 100 | @media (min-width: 1025px) { 101 | display: block !important; 102 | border: none !important; 103 | border-top: 1px solid rgba(0, 0, 0, 0.12) !important; 104 | margin: 7px 0 !important; 105 | } 106 | } 107 | 108 | div.drawerSubContent { 109 | 110 | @media (min-width: 1025px) { 111 | display: block !important; 112 | } 113 | 114 | a { 115 | text-decoration: none; 116 | color: inherit !important; 117 | 118 | &[class=active] div { 119 | color: $primary-color; 120 | background: #F5F5F5; 121 | 122 | @media (min-width: 1025px) { 123 | background: rgba(0, 0, 0, .05); 124 | } 125 | 126 | span i { 127 | color: $primary-color; 128 | } 129 | } 130 | 131 | div { 132 | font-size: 16px; 133 | color: $primary-text-color; 134 | padding: 8px 0 14px 14px; 135 | font-weight: 400; 136 | user-select: none; 137 | 138 | &:hover { 139 | background: #F5F5F5; 140 | color: $primary-color; 141 | cursor: pointer; 142 | 143 | @media (min-width: 1025px) { 144 | background: rgba(0, 0, 0, .05); 145 | } 146 | 147 | } 148 | 149 | &:hover span i { 150 | color: inherit !important; 151 | } 152 | 153 | span i { 154 | position: relative; 155 | top: 6px; 156 | margin-right: 30px; 157 | color: $secondary-text-color; 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | 165 | div.drawerBack { 166 | background: rgba(0, 0, 0, 0.52); 167 | display: none; 168 | height: 100%; 169 | z-index: 99; 170 | position: fixed; 171 | width: 100%; 172 | top: 0; 173 | margin: 0; 174 | 175 | @media (min-width: 1025px) { 176 | display: none !important; 177 | } 178 | } 179 | 180 | body[class=nightmode] { 181 | .nav { 182 | background: #303030 !important; 183 | 184 | a[class=active] div { 185 | background: rgba(255, 255, 255, .05) !important; 186 | color: rgba(255,255,255,.87) !important; 187 | 188 | @media (min-width: 1025px) { 189 | background: rgba(255, 255, 255, .05) !important; 190 | } 191 | 192 | span i { 193 | color: rgba(255, 255, 255, .87) !important; 194 | } 195 | } 196 | 197 | hr { 198 | @media (min-width: 1025px) { 199 | border-top: 1px solid rgba(255, 255, 255, .12) !important; 200 | } 201 | } 202 | 203 | a:hover div { 204 | background-color: rgba(255, 255, 255, .05) !important; 205 | } 206 | 207 | div { 208 | color: rgba(255, 255, 255, .57) !important; 209 | 210 | &:hover { 211 | color: rgba(255, 255, 255, .87) !important; 212 | } 213 | 214 | span i { 215 | color: rgba(255,255,255,.57) !important; 216 | } 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /src/components/radioInput/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | import style from './style'; 4 | 5 | const RadioInput = props => ( 6 |
7 | 8 | 9 |
10 | ); 11 | 12 | export default RadioInput; -------------------------------------------------------------------------------- /src/components/radioInput/style.scss: -------------------------------------------------------------------------------- 1 | @keyframes ripple { 2 | 0% { 3 | box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.0); 4 | } 5 | 50% { 6 | box-shadow: 0px 0px 0px 15px rgba(0, 0, 0, 0.13); 7 | } 8 | 100% { 9 | box-shadow: 0px 0px 0px 15px rgba(0, 0, 0, 0); 10 | } 11 | } 12 | 13 | .radio { 14 | margin: 16px 0; 15 | 16 | input[type=radio] { 17 | display: none; 18 | 19 | &:checked+label:before { 20 | border-color: var(--primary-color); 21 | animation: ripple 0.2s linear forwards; 22 | } 23 | 24 | &:checked+label:after { 25 | transform: scale(1) !important; 26 | } 27 | } 28 | 29 | label { 30 | display: inline-block; 31 | height: 20px; 32 | position: relative; 33 | padding: 0 (20px + 10px); 34 | margin-bottom: 0; 35 | cursor: pointer; 36 | vertical-align: bottom; 37 | 38 | &:focus { 39 | outline: none; 40 | 41 | &:before { 42 | box-shadow: 0px 0px 0px 15px rgba(0, 0, 0, 0.13) !important; 43 | } 44 | } 45 | 46 | &:before, &:after { 47 | position: absolute; 48 | content: ''; 49 | border-radius: 50%; 50 | transition: all .22s ease-in-out; 51 | transition-property: transform, border-color, box-shadow; 52 | } 53 | 54 | &:before { 55 | left: 0; 56 | top: 0; 57 | width: 20px; 58 | height: 20px; 59 | border: 2px solid var(--secondary-text-color); 60 | } 61 | 62 | &:after { 63 | top: 5px; 64 | left: 5px; 65 | width: 10px; 66 | height: 10px; 67 | transform: scale(0); 68 | background: var(--primary-color); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/components/selectInput/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style'; 3 | 4 | const SelectInput = props => ( 5 |
6 | 7 | 10 |
11 | ); 12 | 13 | export default SelectInput; -------------------------------------------------------------------------------- /src/components/selectInput/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .select { 4 | 5 | position: relative; 6 | 7 | select { 8 | appearance: none; 9 | font-family: inherit; 10 | background-color: transparent; 11 | width: 100%; 12 | padding: 4px 0 2px 3px; 13 | margin-top: 24px; 14 | font-size: 18px; 15 | color: $primary-text-color; 16 | border: none; 17 | border-bottom: 1px solid #757575; 18 | 19 | &:focus { 20 | padding: 4px 0 1px 3px; 21 | border-bottom: 2px solid $secondary-color-dark; 22 | outline: none 23 | } 24 | } 25 | 26 | select[incomplete=true] { 27 | padding: 4px 0 1px 3px; 28 | border-bottom: 2px solid #ff1945 !important; 29 | } 30 | 31 | label { 32 | display: none; 33 | } 34 | 35 | &:after { 36 | position: absolute; 37 | bottom: 0.75em; 38 | right: 0.5em; 39 | width: 0; 40 | height: 0; 41 | padding: 0; 42 | content: ''; 43 | border-left: .25em solid transparent; 44 | border-right: .25em solid transparent; 45 | border-top: .375em solid #999; 46 | pointer-events: none; 47 | } 48 | } -------------------------------------------------------------------------------- /src/components/snackbar/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | 4 | import style from './style'; 5 | 6 | @observer 7 | export default class SnackBar extends Component { 8 | 9 | componentDidUpdate() { 10 | if (document.getElementById('floatingActionButton')) { 11 | if (this.props.stores.uiStore.notification) document.getElementById('floatingActionButton').style.bottom = (this.snackbarElement.clientHeight + 22) + 'px'; 12 | else document.getElementById('floatingActionButton').style.bottom = '22px'; 13 | } 14 | 15 | if (document.getElementById('taskInputForm')) { 16 | if (this.props.stores.uiStore.notification) document.getElementById('taskInputForm').style.bottom = (this.snackbarElement.clientHeight) + 'px'; 17 | else document.getElementById('taskInputForm').style.bottom = '0'; 18 | } 19 | } 20 | 21 | render() { 22 | 23 | const { notification } = this.props.stores.uiStore; 24 | 25 | const clickAction = () => { 26 | this.props.stores.uiStore.notification = null; 27 | if (notification && notification.action) notification.action(); 28 | }; 29 | 30 | return ( 31 |
this.snackbarElement = el} style={{ transform: (notification && notification.text) ? 'translateY(0%)' : 'translateY(200%)' }} class={style.snackbar}> 32 | {(notification && notification.text) ? notification.text : null} 33 | 36 |
37 | ); 38 | } 39 | } -------------------------------------------------------------------------------- /src/components/snackbar/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .snackbar { 4 | z-index: 11; 5 | text-align: left; 6 | box-shadow: 0 3px 6px; 7 | background: #323232; 8 | position: fixed; 9 | bottom: 0; 10 | font-size: 16px; 11 | user-select: none; 12 | width: 100vw; 13 | transition: transform .22s; 14 | 15 | @media only screen and (min-width: 700px) { 16 | min-width: 288px; 17 | max-width: 568px; 18 | border-radius: 2px; 19 | margin-bottom: 24px; 20 | left: 24px !important; 21 | } 22 | 23 | span { 24 | color: rgba(255,255,255,.87); 25 | user-select: none; 26 | float: left; 27 | padding: 20px; 28 | } 29 | 30 | button { 31 | background: transparent; 32 | border: none; 33 | color: $secondary-color; 34 | user-select: none; 35 | float: right; 36 | cursor: pointer; 37 | padding: 8px 12px; 38 | border-radius: 3px; 39 | margin: 12px; 40 | font-size: 16px; 41 | outline: none; 42 | 43 | &:hover, &:active { 44 | background: rgba(255,255,255,.13); 45 | } 46 | 47 | &:empty { 48 | display: none; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/components/textInput/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style'; 3 | 4 | const TextInput = props => ( 5 |
6 | 16 | 17 | 18 |
19 | ); 20 | 21 | export default TextInput; -------------------------------------------------------------------------------- /src/components/textInput/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .inputGroup { 4 | position: relative; 5 | 6 | input[type=text], input[type=password] { 7 | background: transparent; 8 | font-size: 18px; 9 | padding: 28px 0 2px 3px; 10 | display: block; 11 | width: 100%; 12 | border: none; 13 | border-bottom: 1px solid #757575; 14 | appearance: none; 15 | 16 | &:focus { 17 | outline: none; 18 | } 19 | 20 | &:focus~label, &:valid~label { 21 | bottom: 26px; 22 | font-size: 14px; 23 | color: var(--secondary-color-dark); 24 | } 25 | 26 | &:focus~label.important, &:valid~label.important { 27 | color: var(--secondary-color-dark) !important; 28 | } 29 | 30 | &:focus~span.bar:before, 31 | &:focus~span.bar:after { 32 | width: 50%; 33 | } 34 | 35 | &[incomplete=true]~span.bar:before, 36 | &[incomplete=true]~span.bar:after { 37 | background: #ff1945 !important; 38 | width: 50%; 39 | } 40 | } 41 | 42 | input[incomplete=true]~label { 43 | color: #ff1945 !important; 44 | } 45 | 46 | span.bar { 47 | position: relative; 48 | display: block; 49 | width: 100%; 50 | background: var(--secondary-color-dark); 51 | 52 | &:before, 53 | &:after { 54 | content: ''; 55 | height: 2px; 56 | width: 0; 57 | bottom: 0px; 58 | position: absolute; 59 | background: inherit; 60 | transition: 0.2s ease all; 61 | } 62 | 63 | &:after { 64 | left: 50%; 65 | } 66 | 67 | &:before { 68 | right: 50%; 69 | } 70 | } 71 | 72 | label { 73 | color: #999; 74 | font-size: 18px; 75 | position: absolute; 76 | pointer-events: none; 77 | left: 3px; 78 | bottom: 5px; 79 | transition: 0.2s ease all; 80 | } 81 | } 82 | 83 | body[class=nightmode] { 84 | input[type=text], input[type=password] { 85 | color: rgba(255,255,255,.87); 86 | border-bottom-color: rgba(255,255,255,.57); 87 | } 88 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './style'; 2 | import App from './components/app'; 3 | import Stores from './lib/state/stores'; 4 | 5 | const Mnged = () => ; 6 | 7 | export default Mnged; 8 | -------------------------------------------------------------------------------- /src/lib/firebase.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | apiKey: 'AIzaSyCzGTQ9yO4va-NOlNpqI4VRrzURQu0PQdE', 3 | authDomain: 'managed-me.firebaseapp.com', 4 | databaseURL: 'https://managed-me.firebaseio.com', 5 | projectId: 'managed-me', 6 | storageBucket: 'managed-me.appspot.com', 7 | messagingSenderId: '303837844694' 8 | }; 9 | 10 | const isBrowser = (typeof window !== 'undefined'); 11 | 12 | if (isBrowser) firebase.initializeApp(config); 13 | 14 | export const firestore = (isBrowser) && firebase.firestore(); 15 | export const auth = (isBrowser) && firebase.auth(); 16 | export const messaging = (isBrowser) && firebase.messaging(); 17 | 18 | export const facebookAuthProvider = (isBrowser) && new firebase.auth.FacebookAuthProvider(); 19 | export const githubAuthProvider = (isBrowser) && new firebase.auth.GithubAuthProvider(); 20 | export const twitterAuthProvider = (isBrowser) && new firebase.auth.TwitterAuthProvider(); 21 | export const googleAuthProvider = (isBrowser) && new firebase.auth.GoogleAuthProvider(); -------------------------------------------------------------------------------- /src/lib/state/initializeState.js: -------------------------------------------------------------------------------- 1 | import { firestore } from '../firebase'; 2 | 3 | const initFirestore = uiStore => { 4 | 5 | if (uiStore.wasFirestoreLoaded) return false; 6 | 7 | // FireStore wasn't loaded before: 8 | firestore.enablePersistence() 9 | .then(() => { 10 | console.log('Offline usage of database is now activated!'); 11 | uiStore.firestoreLoaded(); 12 | }) 13 | .catch((err) => { 14 | if (err.code === 'failed-precondition') { 15 | uiStore.showSnackbar( 16 | 'Wasn\'t able to enable offline database. Maybe you have multible tabs of MNGED opened?', 17 | 'OKAY', 18 | 10000 19 | ); 20 | } 21 | else if (err.code === 'unimplemented') { 22 | uiStore.showSnackbar( 23 | 'Your browser doesn\'t support offline databases. To enjoy the full experience, try updating your browser or installing another one!', 24 | 'OKAY', 25 | 10000 26 | ); 27 | } 28 | }); 29 | }; 30 | 31 | const initializeState = (stores, user) => { 32 | 33 | const { uiStore, taskStore, userStore } = stores; 34 | 35 | // enable offline usage for database 36 | initFirestore(uiStore); 37 | 38 | // set user in MobX 39 | userStore.setUser(user); 40 | 41 | // only perform when user logged in 42 | if (user) { 43 | 44 | // update uiStore so it knows user is logged in 45 | uiStore.setUserState(true); 46 | 47 | // set appmode to app to show header etc.. 48 | uiStore.setAppMode('app'); 49 | 50 | firestore 51 | .collection('user-data') 52 | .doc(user.uid) 53 | .get() 54 | .then(doc => userStore.setUser(user, doc.data())); 55 | 56 | // listen for changes in tasks 57 | firestore 58 | .collection('user-data') 59 | .doc(user.uid) 60 | .collection('tasks') 61 | .onSnapshot(snapshot => { 62 | snapshot.docChanges.forEach(docChanges => { 63 | switch (docChanges.type) { 64 | case 'added': 65 | taskStore.addTask(docChanges.doc.id, docChanges.doc.data()); 66 | 67 | firestore.collection('user-data') 68 | .doc(user.uid) 69 | .collection('tasks') 70 | .doc(docChanges.doc.id) 71 | .collection('attachments') 72 | .onSnapshot(snapshotAttachment => { 73 | snapshotAttachment.docChanges.forEach(docChangesAttachment => { 74 | switch (docChangesAttachment.type) { 75 | case 'added': 76 | taskStore.addAttachment(docChanges.doc.id, docChangesAttachment.doc.id, docChangesAttachment.doc.data()); 77 | break; 78 | case 'modified': 79 | taskStore.editAttachment(docChanges.doc.id, docChangesAttachment.doc.id, docChangesAttachment.doc.data()); 80 | break; 81 | case 'removed': 82 | taskStore.removeAttachment(docChangesAttachment.doc.id); 83 | break; 84 | default: 85 | uiStore.throwError('firestore-unexpected-change-type-in-attachment'); 86 | } 87 | }); 88 | }); 89 | 90 | break; 91 | case 'modified': 92 | taskStore.editTask(docChanges.doc.id, docChanges.doc.data()); 93 | break; 94 | case 'removed': 95 | taskStore.removeTask(docChanges.doc.id); 96 | break; 97 | default: 98 | uiStore.throwError('firestore-unexpected-change-type-in-task'); 99 | } 100 | }); 101 | }); 102 | } 103 | 104 | // user not logged in now, but someone was logged in before 105 | else if (uiStore.userLoggedIn) { 106 | 107 | // set appmode to app to hide header etc.. 108 | uiStore.setAppMode('landing'); 109 | 110 | // reset all database-related stores 111 | stores.reset(); 112 | 113 | // make sure uiStore knows that user not logged in and app updated 114 | uiStore.setUserState(false); 115 | } 116 | 117 | // set appmode to app to hide header etc.. 118 | else uiStore.setAppMode('landing'); 119 | 120 | // init UI 121 | uiStore.appIsLoaded(); 122 | }; 123 | 124 | export default initializeState; -------------------------------------------------------------------------------- /src/lib/state/stores/.taskStore.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/lib/state/stores/.taskStore.js.swp -------------------------------------------------------------------------------- /src/lib/state/stores/.userStore.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m4r1vs/mnged/1e8a54855bccbc0548262d6680d2c97fd79eae04/src/lib/state/stores/.userStore.js.swp -------------------------------------------------------------------------------- /src/lib/state/stores/index.js: -------------------------------------------------------------------------------- 1 | import { observable, useStrict, action } from 'mobx'; 2 | useStrict(); 3 | 4 | import TaskStore from './taskStore'; 5 | import UiStore from './uiStore'; 6 | import UserStore from './userStore'; 7 | 8 | export class Stores { 9 | @observable taskStore = new TaskStore() 10 | @observable uiStore = new UiStore() 11 | @observable userStore = new UserStore() 12 | 13 | /** 14 | * Resets all database-related stores to initial state 15 | */ 16 | @action reset() { 17 | this.taskStore = new TaskStore(); 18 | } 19 | 20 | } 21 | 22 | export default new Stores(); -------------------------------------------------------------------------------- /src/lib/state/stores/taskStore.js: -------------------------------------------------------------------------------- 1 | import { observable, computed, action } from 'mobx'; 2 | 3 | class Attachment { 4 | @observable id 5 | @observable type 6 | @observable title 7 | @observable content 8 | @observable created 9 | 10 | constructor (id, attachment) { 11 | this.id = id; 12 | this.type = attachment.type; 13 | this.title = attachment.title; 14 | this.content = attachment.content; 15 | this.created = attachment.created; 16 | } 17 | } 18 | 19 | class Task { 20 | @observable id 21 | @observable title 22 | @observable attachments 23 | @observable due 24 | @observable created 25 | @observable group 26 | @observable done 27 | 28 | constructor(id, task) { 29 | this.id = id; 30 | this.title = task.title || 'empty task'; 31 | this.attachments = []; 32 | this.due = task.due || null; 33 | this.created = task.created || new Date(); 34 | this.group = task.group || null; 35 | this.done = task.done || false; 36 | } 37 | 38 | /** 39 | * Return a color of group for task 40 | */ 41 | @computed get colorByGroup() { 42 | // TODO: make it real 43 | switch (this.group) { 44 | case 'red': 45 | return '#ef5350'; 46 | case 'blue': 47 | return '#42a5f5'; 48 | case 'purple': 49 | return '#ab47bc'; 50 | case 'orange': 51 | return '#ffa726'; 52 | case 'green': 53 | return '#66bb6a'; 54 | default: 55 | return '#78909c'; 56 | } 57 | } 58 | 59 | /** 60 | * Returns number of attachments, pretty self-explaning, eh? 61 | */ 62 | @computed get numberOfAttachments() { 63 | return (this.attachments.length > 0) && this.attachments.length; 64 | } 65 | 66 | /** 67 | * Returns a list of attachments ordered by created date 68 | */ 69 | @computed get listOfAttachments() { 70 | const attachmentList = this.attachments.sort((a, b) => b.created.getTime() - a.created.getTime()); 71 | return attachmentList; 72 | } 73 | 74 | /** 75 | * Returns the last two attachments 76 | */ 77 | @computed get firstTwoAttachments() { 78 | return this.listOfAttachments.slice(0, 2); 79 | } 80 | 81 | /** 82 | * the time left in the task 83 | */ 84 | @computed get timeLeft() { 85 | if (!this.due) return null; 86 | const millisToTime = (millisec) => { 87 | if (millisec < 0) return 'overdue'; 88 | let seconds = (millisec / 1000).toFixed(0); 89 | let hours = Math.floor(seconds / 3600); 90 | let days = ''; 91 | 92 | if (hours < 24) return hours + 'h'; 93 | days = Math.floor(hours / 24); 94 | hours = hours - (days * 24); 95 | return days + 'd ' + hours + 'h'; 96 | }; 97 | 98 | const now = new Date(); 99 | const dueDate = this.due; 100 | const newDate = new Date(dueDate - now); 101 | return millisToTime(newDate); 102 | } 103 | 104 | /** 105 | * Returns the time left as a readable date 106 | */ 107 | @computed get timeReadable() { 108 | if (!this.due) return 'not set'; 109 | const dueDate = this.due; 110 | const now = new Date(); 111 | 112 | const months = [ 113 | 'Jan', 114 | 'Feb', 115 | 'Mar', 116 | 'Apr', 117 | 'May', 118 | 'Jun', 119 | 'Jul', 120 | 'Aug', 121 | 'Sep', 122 | 'Okt', 123 | 'Nov', 124 | 'Dec' 125 | ]; 126 | 127 | const month = months[dueDate.getMonth()]; 128 | const day = dueDate.getDate(); 129 | let hours = dueDate.getHours(); 130 | let minutes = dueDate.getMinutes(); 131 | 132 | let date = month + ' ' + day; 133 | if (now.getDate() === day && now.getMonth() === dueDate.getMonth() && now.getFullYear() === dueDate.getFullYear()) date = 'Today'; 134 | if (now.getDate() === day - 1 && now.getMonth() === dueDate.getMonth() && now.getFullYear() === dueDate.getFullYear()) date = 'Tomorrow'; 135 | if (now.getDate() === day + 1 && now.getMonth() === dueDate.getMonth() && now.getFullYear() === dueDate.getFullYear()) date = 'Yesterday'; 136 | 137 | if (hours < 10) hours = '0' + hours; 138 | if (minutes < 10) minutes = '0' + minutes; 139 | 140 | const time = hours + ':' + minutes; 141 | 142 | return date + ', ' + time; 143 | } 144 | 145 | /** 146 | * Edit an attachment 147 | * @param {string} id the id of the attachment 148 | * @param {object} attachment the attachment 149 | */ 150 | @action editAttachment(id, attachment) { 151 | for (let i = 0; i < this.attachments.length; i++) { 152 | if (this.attachments[i].id === id) this.attachments[i] = new Attachment(id, attachment); 153 | } 154 | } 155 | } 156 | 157 | 158 | export default class TaskStore { 159 | @observable tasks = [] 160 | 161 | /** 162 | * get the tasks ordered by date 163 | */ 164 | @computed get taskList() { 165 | 166 | const taskList = { 167 | overdue: [], 168 | next: [], 169 | later: [], 170 | notDue: [] 171 | }; 172 | 173 | this.tasks.forEach(task => { 174 | if (!task.due) taskList.later.push(task); 175 | else if (task.timeLeft === 'overdue') taskList.overdue.push(task); 176 | else if (task.due.getTime() - new Date().getTime() > 604799999) taskList.notDue.push(task); 177 | else taskList.next.push(task); 178 | }); 179 | 180 | return { 181 | overdue: taskList.overdue.sort((a, b) => a.due.getTime() - b.due.getTime()), 182 | next: taskList.next.sort((a, b) => a.due.getTime() - b.due.getTime()), 183 | later: taskList.later, 184 | notDue: taskList.notDue.sort((a, b) => a.due.getTime() - b.due.getTime()) 185 | }; 186 | 187 | } 188 | 189 | /** 190 | * append the taskstore with a new task 191 | * @param {string} id the id of the new task 192 | * @param {object} task the task 193 | */ 194 | @action addTask(id, task) { 195 | this.tasks.push(new Task(id, task)); 196 | } 197 | 198 | /** 199 | * edit a task in the taskstore 200 | * @param {string} id the id of the new task 201 | * @param {object} taskNew the new task 202 | */ 203 | @action editTask(id, taskNew) { 204 | for (let i = 0; i < this.tasks.length; i++) { 205 | if (this.tasks[i].id === id) this.tasks[i] = new Task(id, taskNew); 206 | } 207 | } 208 | 209 | /** 210 | * remove a task from the taskstore 211 | * @param {string} id the id of the task to be removed 212 | */ 213 | @action removeTask(id) { 214 | for (let i = 0; i < this.tasks.length; i++) { 215 | if (this.tasks[i].id === id) this.tasks.splice(i, 1); 216 | } 217 | } 218 | 219 | /** 220 | * add an attachment to a task 221 | * @param {string} taskId the id of the task the attachment should be added to 222 | * @param {string} attachmentId the id of the new attachment 223 | * @param {object} attachment the attachment itself 224 | */ 225 | @action addAttachment(taskId, attachmentId, attachment) { 226 | for (let i = 0; i < this.tasks.length; i++) { 227 | if (this.tasks[i].id === taskId) this.tasks[i].attachments.push(new Attachment(attachmentId, attachment)); 228 | } 229 | } 230 | 231 | /** 232 | * edit an attachment 233 | * @param {string} taskId the id of the task which the edited attachment belongs t 234 | * @param {string} attachmentId the id of the edited attachment 235 | * @param {object} attachment the attachment itself 236 | */ 237 | @action editAttachment(taskId, attachmentId, attachment) { 238 | for (let i = 0; i < this.tasks.length; i++) { 239 | if (this.tasks[i].id === taskId) this.tasks[i].editAttachment(attachmentId, attachment); 240 | } 241 | } 242 | } -------------------------------------------------------------------------------- /src/lib/state/stores/uiStore.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class SubPage { 4 | @observable headerTitle 5 | @observable headerColor 6 | @observable headerAction 7 | @observable headerActionIcon 8 | 9 | constructor(details) { 10 | this.headerTitle = details.headerTitle; 11 | this.headerColor = details.headerColor; 12 | this.headerAction = details.headerAction; 13 | this.headerActionIcon = details.headerActionIcon; 14 | } 15 | } 16 | 17 | class DatePicker { 18 | @observable opened 19 | @observable recieverFunction 20 | 21 | @action open(recieverFunction) { 22 | this.opened = true; 23 | this.recieverFunction = recieverFunction; 24 | } 25 | 26 | @action close() { 27 | this.opened = false; 28 | this.recieverFunction = () => false; 29 | } 30 | } 31 | 32 | export default class UiStore { 33 | @observable notification = null 34 | @observable dialog = null 35 | @observable error = null 36 | @observable appLoaded = false 37 | @observable userLoggedIn = false 38 | @observable subPage = false 39 | @observable wasFirestoreLoaded = false 40 | @observable appMode = null 41 | @observable datePicker = new DatePicker() 42 | 43 | /** 44 | * set app loaded to true 45 | */ 46 | @action appIsLoaded() { 47 | this.appLoaded = true; 48 | } 49 | 50 | /** 51 | * Set the mode of the app 52 | * @param {string} mode app or landing 53 | */ 54 | @action setAppMode(mode) { 55 | switch (mode) { 56 | case 'app': 57 | this.appMode = 'app'; 58 | break; 59 | case 'landing': 60 | this.appMode = 'landing'; 61 | break; 62 | default: 63 | this.throwError('tried-to-assign-unknown-appmode'); 64 | } 65 | } 66 | 67 | /** 68 | * Throw a fatal error which causes the whole UI to freeze and display the error. For non-fatal error use snackbar 69 | * @param {string} code an errorcode looking like following: 'firestore-error-creating-task' 70 | * @param {string} [info] further information to display with the error 71 | */ 72 | @action throwError(code, info) { 73 | if (info) this.error = info + ' (Error: ' + code + ')'; 74 | else this.error = 'Something went wrong. Error: ' + code + '. Please consider contacting us under feedback or via Twitter @MariusNiveri'; 75 | console.error(this.error); 76 | } 77 | 78 | /** 79 | * Set Firestore loaded to true, so offline persistance doesn't get activated twice 80 | */ 81 | @action firestoreLoaded() { 82 | this.wasFirestoreLoaded = true; 83 | } 84 | 85 | /** 86 | * Show a snackbar 87 | * @param {string} text A text to display in the snackbar 88 | * @param {string} [actionText] text of button 89 | * @param {number} time how many ms should the snackbar be displayed 90 | * @param {function} [action] a function to get executed when button clicked 91 | */ 92 | @action showSnackbar(text, actionText, time, action) { 93 | if (this.notification && this.notification.timeout) clearTimeout(this.notification.timeout); 94 | if (this.notification) { 95 | this.notification = null; 96 | this.notification = { 97 | timeout: setTimeout(() => { 98 | this.notification = { 99 | text, 100 | action, 101 | actionText, 102 | ...this.notification 103 | }; 104 | this.notification.timeout = setTimeout(() => this.notification = null, time); 105 | }, 500) 106 | }; 107 | } 108 | else { 109 | this.notification = { 110 | text, 111 | action, 112 | actionText 113 | }; 114 | this.notification.timeout = setTimeout(() => this.notification = null, time); 115 | } 116 | } 117 | 118 | /** 119 | * Opens a new dialog window 120 | * @param {string} title the title of the dialog 121 | * @param {jsx} content the html inside the dialog 122 | */ 123 | @action openDialog(title, content, details) { 124 | this.dialog = { 125 | title, 126 | content, 127 | details, 128 | opened: true 129 | }; 130 | } 131 | 132 | /** 133 | * Closes an opened dialog or does nothing if already closed 134 | */ 135 | @action closeDialog() { 136 | if (this.dialog) this.dialog.opened = false; 137 | } 138 | 139 | /** 140 | * Transition the header to a sub-page mode 141 | * @param {string} details.headerTitle Title of page displayed in header 142 | * @param {string} details.headerColor Color of header 143 | * @param {function} details.headerAction Gets executed when clicked on action 144 | * @param {string} details.headerActionIcon icon from MD-icons to show at top right 145 | */ 146 | @action setSubPage(details) { 147 | if (details) { 148 | this.subPage = new SubPage(details); 149 | } 150 | else this.subPage = false; 151 | 152 | if (typeof window !== 'undefined') { 153 | const themeColor = document.querySelector('meta[name=theme-color]') || null; 154 | if (themeColor) themeColor.setAttribute('content', details ? (details.headerColor || '#282d8c') : '#282d8c'); 155 | } 156 | } 157 | 158 | /** 159 | * Function to set user state 160 | * @param {boolean} user if user is logged in or not 161 | */ 162 | @action setUserState(user) { 163 | this.userLoggedIn = user; 164 | } 165 | } -------------------------------------------------------------------------------- /src/lib/state/stores/userStore.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class User { 4 | @observable name 5 | @observable email 6 | @observable photoURL 7 | @observable headerURL 8 | 9 | constructor(user, databaseUser) { 10 | this.name = user.displayName || 'Mnger'; 11 | this.email = user.email || 'Not provided'; 12 | this.uid = user.uid || null; 13 | this.photoURL = user.photoURL || null; 14 | this.headerURL = databaseUser ? databaseUser.headerURL : null; 15 | } 16 | } 17 | 18 | export default class UserStore { 19 | @observable user = {} 20 | 21 | /** 22 | * sets up representation of user in mobx 23 | * @param {object} user user object 24 | */ 25 | @action setUser(user, databaseUser) { 26 | if (user) this.user = databaseUser ? new User(user, databaseUser) : new User(user); 27 | else this.user = null; 28 | } 29 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Managed Me", 3 | "short_name": "MNGED", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#FAFAFA", 8 | "theme_color": "#0e1460", 9 | "gcm_sender_id": "103953800507", 10 | "icons": [ 11 | { 12 | "src": "/assets/icons/android-chrome-192x192.png", 13 | "type": "image/png", 14 | "sizes": "192x192" 15 | }, 16 | { 17 | "src": "/assets/icons/android-chrome-256x256.png", 18 | "type": "image/png", 19 | "sizes": "256x256" 20 | }, 21 | { 22 | "src": "/assets/icons/android-chrome-512x512.png", 23 | "type": "image/png", 24 | "sizes": "512x512" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/routes/about/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import style from './style'; 3 | 4 | import Card from '../../components/card'; 5 | 6 | const About = () => ( 7 |
8 | 9 |

General

10 |

11 | This project started as a project for my High School's IT class and after I handed it in I asked myself why not continue working on MNGED since I always 12 | struggled finding a good task manager for myself that isn't too feature rich like evernote and listens to their users unlike Google Keep. 13 | So this is what I came up with. MNGED is still under heavy development but it won't hopefully take that long until v1.0 hopefully. So stay tuned for updates and report any issues and ideas to me 14 |

15 |
16 |

Security

17 |

18 | MNGED uses Firebase for authentication and as a database. Firesbase is developed and maintained 19 | by Google. The authentication is build by the same team that also build Google Sign In and is responsible for other security at Google. 20 | But that also means that Google has access to our database which you may or may not care about. 21 |

22 |
23 |

How it's made

24 |

25 | These sort of Web applications are called Progressive Web Apps (PWA). 26 | PWA's stand out because they are fast and always work, 27 | even with no connection to the internet. 28 |

29 |

30 | As the UI provider I decided to go with Preact, a lightweight 3kb fork of React. 31 | For storing the state I use MobX, it's a simple but powerful state management solution. 32 | And finally as the database I went with Firebase, a mostly free hosting and database provided by Google. 33 | The nice thing about firebase is that it comes with a nice JavaScript library which enables Authentication and live-updates when the database changes. 34 |

35 |
36 |

Links

37 | 43 |
44 |
45 | ); 46 | 47 | export default About; -------------------------------------------------------------------------------- /src/routes/about/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .about { 4 | padding: 12px 0px 12px 0px; 5 | width: 100%; 6 | background: #fff; 7 | 8 | @media (min-width: 500px) { 9 | background: transparent !important; 10 | } 11 | } 12 | 13 | body[class=nightmode] { 14 | .about { 15 | background: #424242; 16 | } 17 | } -------------------------------------------------------------------------------- /src/routes/class/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | import { route } from 'preact-router'; 4 | import style from './style'; 5 | 6 | import Card from '../../components/card'; 7 | @observer 8 | export default class Class extends Component { 9 | 10 | componentDidMount() { 11 | this.props.stores.classesStore.setDisplayedClass(this.props.class); 12 | const displayedClass = this.props.stores.classesStore.getDisplayedClass; 13 | 14 | this.props.stores.uiStore.setSubPage({ 15 | headerTitle: displayedClass ? displayedClass.name : 'Class not found', 16 | headerColor: displayedClass ? displayedClass.color : null, 17 | headerAction: () => displayedClass ? route('/edit-class/'+displayedClass.name) : () => this.props.stores.uiStore.showSnackbar('No class selected', null, 5000), 18 | headerActionIcon: 'edit' 19 | }); 20 | } 21 | 22 | componentWillUnmount() { 23 | this.props.stores.uiStore.setSubPage(false); 24 | } 25 | 26 | render({ stores }) { 27 | 28 | const { taskList } = stores.taskStore; 29 | const displayedClass = stores.classesStore.getDisplayedClass; 30 | 31 | return ( 32 |
33 | 34 | 35 | 36 | {displayedClass ?
37 |
38 | 39 | 10:30 - 11:40 AM Today 40 |
41 |
42 | 43 | {displayedClass.teacher} 44 |
45 |
46 |  47 | {displayedClass.room} 48 |
49 |

Tasks due for this class

50 | 51 | {taskList.map((task) => { 52 | if (task.subjectRef === displayedClass.id) { 53 | return ( 54 |
route('/task/' + task.id, false)}> 55 |
56 | {task.timeLeft} 57 |
{task.title}
58 | {task.body} 59 |
60 | ); 61 | } 62 | })} 63 | 64 |
:
65 | Class not found: {this.props.class} 66 |
} 67 | 68 | 69 |
70 | ); 71 | } 72 | } -------------------------------------------------------------------------------- /src/routes/class/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .class { 4 | padding: 132px 0 8px 0; 5 | height: 100vh; 6 | background: #fff; 7 | 8 | .card { 9 | padding: 0; 10 | } 11 | 12 | div.listElement { 13 | position: relative; 14 | padding: 12px 16px; 15 | min-height: 56px; 16 | 17 | i { 18 | font-size: 32px; 19 | } 20 | 21 | span { 22 | position: relative; 23 | top: -10px; 24 | left: 16px; 25 | } 26 | } 27 | 28 | h4 { 29 | padding: 0; 30 | margin: 0; 31 | margin: 8px 16px; 32 | } 33 | 34 | div.taskListElement { 35 | 36 | cursor: pointer; 37 | min-height: 58px; 38 | padding: 8px 16px; 39 | position: relative; 40 | margin-bottom: 12px; 41 | 42 | h5 { 43 | margin: 0 0 4px 0; 44 | font-size: 16px; 45 | padding: 0; 46 | font-weight: 400; 47 | } 48 | 49 | span.taskDescription { 50 | font-size: 14px; 51 | line-height: 16px; 52 | color: $secondary-text-color; 53 | } 54 | 55 | span.taskTimeLeft { 56 | font-size: 14px; 57 | line-height: 16px; 58 | color: $secondary-text-color; 59 | float: right; 60 | } 61 | 62 | div.colorIndicator { 63 | width: 4px; 64 | margin-left: -16px; 65 | top: 0; 66 | bottom: 0; 67 | position: absolute; 68 | } 69 | 70 | &:active { 71 | filter: brightness(.87); 72 | } 73 | } 74 | } 75 | 76 | @media (min-width: 1025px) { 77 | .class { 78 | 79 | padding: 0 0 8px 0; 80 | height: auto !important; 81 | min-height: 400px; 82 | width: 450px; 83 | margin: 0 auto; 84 | position: relative; 85 | top: 106px; 86 | background: transparent !important; 87 | 88 | div.listElement { 89 | padding: 12px 16px; 90 | height: 56px; 91 | border-bottom: 1px solid rgba(0, 0, 0, .13); 92 | i { 93 | font-size: 32px; 94 | } 95 | span { 96 | position: relative; 97 | top: -10px; 98 | left: 16px; 99 | } 100 | } 101 | } 102 | } 103 | 104 | body[class=nightmode] { 105 | .class { 106 | background: #424242; 107 | h5 { color: rgba(255, 255, 255, 0.87); } 108 | i, span { color: rgba(255, 255, 255, 0.87); } 109 | span.taskDescription, span.taskTimeLeft { color: rgba(255, 255, 255, 0.87) !important; } 110 | } 111 | } -------------------------------------------------------------------------------- /src/routes/dashboard/classList/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { route } from 'preact-router'; 3 | import style from './style'; 4 | 5 | const classList = props => { 6 | 7 | const routeToClass = () => route('/class/' + props.block.name, false); 8 | 9 | return ( 10 |
11 |
12 |

{props.block.name}

13 | {props.block.room} 14 | {props.block.teacher} 15 |
16 | ); 17 | } 18 | 19 | export default classList; -------------------------------------------------------------------------------- /src/routes/dashboard/classList/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/vars.scss'; 2 | 3 | .classList { 4 | 5 | position: relative; 6 | width: 100%; 7 | background-color: #fff; 8 | box-shadow: $card-box-shadow; 9 | margin-bottom: 10px; 10 | height: 92px; 11 | padding: 25px 20px 25px 27px; 12 | cursor: pointer; 13 | 14 | &:active { 15 | filter: brightness(.87); 16 | } 17 | 18 | @media (min-width: 500px) { 19 | border-radius: 3px; 20 | } 21 | 22 | div.colorIndicator { 23 | top: 0; 24 | bottom: 0; 25 | width: 7px; 26 | margin-left: -27px; 27 | position: absolute; 28 | 29 | @media (min-width: 500px) { 30 | border-top-left-radius: 3px; 31 | border-bottom-left-radius: 3px; 32 | } 33 | } 34 | 35 | h3 { 36 | color: $primary-text-color; 37 | margin: 0; 38 | padding: 0; 39 | font-weight: 400; 40 | } 41 | 42 | span.room { 43 | color: $secondary-text-color; 44 | } 45 | 46 | span.teacher { 47 | color: $secondary-text-color; 48 | font-size: 14px; 49 | float: right; 50 | vertical-align: bottom; 51 | 52 | i { 53 | color: $secondary-text-color; 54 | font-size: 16px; 55 | margin: 0; 56 | padding: 0; 57 | position: absolute; 58 | margin-left: -20px; 59 | margin-top: -1px; 60 | } 61 | } 62 | } 63 | 64 | body[class=nightmode] { 65 | .classList { 66 | background: #424242; 67 | 68 | &:active { 69 | filter: brightness(1.13); 70 | } 71 | 72 | h3 { 73 | color: rgba(255,255,255,.87); 74 | } 75 | 76 | span, i { 77 | color: rgba(255,255,255,.57); 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/routes/dashboard/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | import style from './style'; 4 | 5 | import DayNavigator from '../../components/dayNavigator'; 6 | import ClassList from './classList'; 7 | import SetClasses from './setClasses'; 8 | 9 | @observer 10 | export default class Dashboard extends Component { 11 | 12 | getDate(date) { 13 | 14 | const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 15 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' 16 | ]; 17 | 18 | const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 19 | 20 | let d = new Date(date); 21 | const now = this.props.stores.uiStore.currentTime; 22 | 23 | if (d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) return 'Today'; 24 | 25 | const month = monthNames[d.getMonth()]; 26 | const dayName = dayNames[d.getDay()]; 27 | 28 | let day = d.getDate(); 29 | 30 | if (day <= 1) day = day + 'st'; 31 | else day = day + 'th'; 32 | 33 | return dayName + ', ' + month + ' ' + day; 34 | } 35 | 36 | nextDay() { 37 | const currentState = this.state.displayedDate; 38 | this.props.stores.classesStore.changeDisplayedClasses(currentState + 86400000); 39 | this.setState({ displayedDate: currentState + 86400000 }); 40 | } 41 | 42 | previosDay() { 43 | const currentState = this.state.displayedDate; 44 | this.props.stores.classesStore.changeDisplayedClasses(currentState - 86400000); 45 | this.setState({ displayedDate: currentState - 86400000 }); 46 | } 47 | 48 | constructor(props) { 49 | super(props); 50 | this.state = { 51 | displayedDate: props.stores.uiStore.currentTime.getTime() 52 | }; 53 | } 54 | 55 | componentDidMount() { 56 | const currentState = this.state.displayedDate; 57 | this.props.stores.classesStore.changeDisplayedClasses(currentState); 58 | } 59 | 60 | render() { 61 | 62 | const { newUser } = this.props.stores.uiStore; 63 | 64 | const classes = this.props.stores.classesStore.filteredClasses; 65 | 66 | const renderClasses = ms => ( 67 |
68 | 74 |

{classes.notes}

75 | {classes && classes.classes && classes.classes.map((subject) => ( 76 | 77 | ))} 78 |
79 | ); 80 | 81 | if (newUser) return ; 82 | return ( 83 |
84 | {renderClasses()} 85 |
86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/routes/dashboard/setClasses/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | import { firestore, auth } from '../../../lib/firebase'; 4 | import TextInput from '../../../components/textInput'; 5 | import DefaultButton from '../../../components/button'; 6 | import Card from '../../../components/card'; 7 | import style from './style'; 8 | 9 | const template = [ 10 | [ 'block_a', 'Block A' ], 11 | [ 'block_b', 'Block B' ], 12 | [ 'block_c', 'Block C' ], 13 | [ 'block_d', 'Block D' ], 14 | [ 'block_e', 'Block E' ], 15 | [ 'block_f', 'Block F' ], 16 | [ 'block_g', 'Block G' ], 17 | [ 'block_h', 'Block H' ], 18 | [ 'flex', 'Flex' ] 19 | ]; 20 | 21 | let myState = { 22 | block_a: {}, 23 | block_b: {}, 24 | block_c: {}, 25 | block_d: {}, 26 | block_e: {}, 27 | block_f: {}, 28 | block_g: {}, 29 | block_h: {}, 30 | flex: { 31 | class: 'Flex' 32 | } 33 | }; 34 | 35 | const colors = [ 36 | '#d32f2f', 37 | '#7b1fa2', 38 | '#1976d2', 39 | '#00897b', 40 | '#7cb342', 41 | '#ff8f00', 42 | '#795548', 43 | '#546e7a' 44 | ] 45 | 46 | export default class SetClass extends Component { 47 | 48 | handleChange(evt) { 49 | myState[evt.target.name.split('-')[0]][evt.target.name.split('-')[1]] = evt.target.value; 50 | } 51 | 52 | proccessForm(evt) { 53 | evt.preventDefault(); 54 | console.log(auth.currentUser.uid); 55 | 56 | const batch = firestore.batch(); 57 | 58 | const classesRef = firestore 59 | .collection('users') 60 | .doc(auth.currentUser.uid) 61 | .collection('classes'); 62 | 63 | for (let key in myState) { 64 | 65 | if (!myState.hasOwnProperty(key)) continue; 66 | 67 | batch.set(classesRef.doc(key), myState[key]); 68 | } 69 | 70 | batch.commit().then(() => { 71 | console.log('done'); 72 | }); 73 | 74 | } 75 | 76 | constructor() { 77 | super(); 78 | this.state = null; 79 | } 80 | 81 | renderColorInput(color, item) { 82 | return ( 83 | 91 | ) 92 | } 93 | 94 | renderInput(item) { 95 | return ( 96 |
97 |

{item[1]}:

98 | {item[0] !== 'flex' && } 99 | 100 | 101 |
102 |
103 | {colors.map((color, i) => this.renderColorInput(color, item))} 104 |
105 |
106 |
); 107 | } 108 | 109 | render() { 110 | return ( 111 |
112 | 113 |

Set classes

114 |

Before you can start using mnged, we need your schedule. Please insert your class, room and teacher for the corresponding block. You are also able to select a color assosiated with that class:

115 |
116 | {template.map((item, i) => ( 117 |
{this.renderInput(item)}
118 | ))} 119 | 120 |
121 |
122 |
123 | ); 124 | } 125 | } -------------------------------------------------------------------------------- /src/routes/dashboard/setClasses/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/vars.scss'; 2 | 3 | .wrapper { 4 | margin-top: 56px; 5 | width: 100%; 6 | background: #fff; 7 | 8 | @media (min-width: 500px) { 9 | width: 450px; 10 | margin: 56px auto; 11 | background: transparent; 12 | border-radius: 3px; 13 | } 14 | 15 | .card { 16 | padding-bottom: 18px; 17 | } 18 | 19 | h3, h4 { 20 | padding: 15px 0 0 0; 21 | line-height: 28px; 22 | font-size: 20px; 23 | color: $secondary-text-color; 24 | font-weight: 700; 25 | margin: 0; 26 | } 27 | 28 | h3 { 29 | padding: 15px 0; 30 | font-size: 24px; 31 | } 32 | 33 | p { 34 | padding: 0 0 15px 0; 35 | line-height: 24px; 36 | margin: 0; 37 | font-size: 14px; 38 | color: $primary-text-color; 39 | border-bottom: 1px solid rgba(0,0,0,.13); 40 | 41 | a { 42 | color: $secondary-color-dark; 43 | text-decoration: none; 44 | } 45 | } 46 | 47 | input[type=submit] { 48 | cursor: pointer; 49 | display: inline-block; 50 | float: right; 51 | position: relative; 52 | min-width: 120px; 53 | max-width: 140px; 54 | padding: 6px; 55 | margin: 6px 6px 0 0; 56 | border-radius: 2px; 57 | text-align: center; 58 | font-size: 16px; 59 | text-decoration: none; 60 | border: none; 61 | background-color: $secondary-color-dark; 62 | color: #fff; 63 | outline: none; 64 | transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1); 65 | transition-delay: 0.2s; 66 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); 67 | } 68 | } 69 | 70 | .input { 71 | 72 | margin: 10px; 73 | 74 | input[type=radio] { 75 | appearance: none; 76 | height: 26px; 77 | cursor: pointer; 78 | width: 26px; 79 | background: transparent; 80 | border-radius: 50%; 81 | margin-bottom: 4px; 82 | margin-left: 5px; 83 | margin-right: 5px; 84 | transition-property: transform, border; 85 | transition-duration: .22s; 86 | 87 | &:checked { 88 | border: 2px solid black; 89 | transform: scale(1.3); 90 | } 91 | 92 | &:focus { 93 | outline: none; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/routes/dashboard/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .home { 4 | padding: 64px 0 64px 0; 5 | min-height: 100vh; 6 | width: 100%; 7 | background: transparent; 8 | 9 | @media (min-width: 500px) { 10 | width: 450px; 11 | margin: 0 auto; 12 | background: transparent !important; 13 | } 14 | 15 | div.noSchedule { 16 | color: red; 17 | } 18 | 19 | h4.notes { 20 | padding: 8px 20px 8px 27px; 21 | } 22 | } 23 | 24 | 25 | body[class=nightmode] { 26 | .home { 27 | background: #303030; 28 | } 29 | } -------------------------------------------------------------------------------- /src/routes/donate/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { Link } from 'preact-router/match'; 3 | 4 | import { DoughnutIcon, BeerIcon, PizzaIcon, ChickenIcon, BrushIcon } from '../../components/icons'; 5 | 6 | import Card from '../../components/card'; 7 | import style from './style'; 8 | 9 | export default class Donate extends Component { 10 | 11 | constructor() { 12 | super(); 13 | this.state = null; 14 | } 15 | 16 | componentDidMount() { 17 | document.querySelectorAll('.applyHoverEffect').forEach((element) => { 18 | console.log("lol") 19 | const hoverColor = element.getAttribute('fill'); // get the fill color 20 | 21 | // set it as a custom property inline 22 | if (hoverColor) element.style.setProperty('--hover-color', hoverColor); 23 | }); 24 | } 25 | 26 | shapeshiftClick (e) { 27 | e.preventDefault(); 28 | const link = "https://shapeshift.io/shifty.html?destination=1Lg9BjkTaGkXfhU54LkG91fkQRUunBDSYR&output=BCH&apiKey=9623e7650ab4c717e953bd4d6461fa81372208af22f9b2e56bab1393605a43b2ac7e0db570552fd2073a3c0c5ae9702827e061c2af2d43969e66fdc8f5683835"; 29 | window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); 30 | return false; 31 | } 32 | 33 | componentWillMount() { 34 | fetch('https://maniyt.de/api/crypto/get-value?BTC=1&BROWSER=false') 35 | .then(res => res.json()) 36 | .then(res => this.setState({ price: res.BTC })); 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | 43 |

Donate

44 |

45 |

46 | 47 | 48 | 49 | 50 | 51 |
52 | Hi there, I am happy that you are thinking of giving me a small tip for my work.
53 | Maintaining open source projects like this one is fun and important but bills still have to be payed so every donation is very much appreciated.
54 | You can either choose one of the things above or set the amount you wish to tip :)

55 | 56 | The links above will redirect you to PayPal which I think will be just fine for this purpose and even if you don't have an account you should be able to just use a credit card without having to log in.
57 | But if you have some crypto coins laying around somewhere you also can use those as a tip by using ShapeShift. 58 |

59 |
60 |
61 | ); 62 | } 63 | } -------------------------------------------------------------------------------- /src/routes/donate/style.scss: -------------------------------------------------------------------------------- 1 | .donate { 2 | 3 | padding: 12px 0px 12px 0px; 4 | width: 100%; 5 | background: #fff; 6 | 7 | @media (min-width: 500px) { 8 | background: transparent !important; 9 | } 10 | 11 | .icons { 12 | 13 | text-align: center; 14 | 15 | svg { 16 | margin: 12px; 17 | cursor: pointer; 18 | height: 57px; 19 | position: unset; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/routes/errorpage/index.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | import style from './style'; 4 | 5 | const ErrorPage = () => ( 6 |
7 | ); 8 | 9 | export default ErrorPage; -------------------------------------------------------------------------------- /src/routes/errorpage/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../style/vars.scss'; 2 | 3 | .errorpage { 4 | padding: 56px 20px; 5 | height: 100vh; 6 | max-width: 100vw; 7 | background-position: center; 8 | background-size: contain; 9 | background-repeat: no-repeat; 10 | top: 0; 11 | left: 0; 12 | z-index: 0; 13 | background-image: url('/assets/imgs/404.svg'); 14 | max-width: 500px; 15 | margin: 0 25px; 16 | 17 | @media (min-width: 500px) { 18 | margin: 0 auto; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/routes/home/addTask/index.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import { observer } from 'preact-mobx'; 3 | import { route } from 'preact-router'; 4 | import { firestore, auth } from '../../../lib/firebase'; 5 | 6 | import RadioInput from '../../../components/radioInput'; 7 | 8 | import style from './style'; 9 | 10 | @observer 11 | export default class AddTask extends Component { 12 | 13 | createTask(e) { 14 | e.preventDefault(); 15 | 16 | if (!this.inputTask.value) { 17 | this.props.stores.uiStore.showSnackbar(`Please don't submit an empty Task`, null, 4500); 18 | return false; 19 | } 20 | 21 | firestore 22 | .collection('user-data') 23 | .doc(auth.currentUser.uid) 24 | .collection('tasks') 25 | .add({ 26 | title: this.inputTask.value, 27 | due: this.state.dueDate ? this.state.dueDate : null, 28 | created: new Date(), 29 | group: this.state.selectedGroupColor || null 30 | }) 31 | .then(task => { 32 | this.inputTask.value = ''; 33 | this.setState({ selectedGroupColor: null }); 34 | this.contractForm(); 35 | this.props.stores.uiStore.showSnackbar(`Task successfully created!`, 'SHOW', 4500, () => route('/task/' + task.id)); 36 | }) 37 | .catch(err => { 38 | console.error(err); 39 | this.props.stores.uiStore.showSnackbar(`Error creating Task. See console for technical information`, null, 4500); 40 | }); 41 | } 42 | 43 | expandForm() { 44 | this.colorIndicatorElement.classList.add(style.active); 45 | this.moreActions.style.height = '100%'; 46 | this.moreActions.style.transform = 'scaley(1)'; 47 | this.formElement.style.minHeight = '84px'; 48 | this.formElement.style.height = 'auto'; 49 | } 50 | 51 | contractForm() { 52 | this.colorIndicatorElement.classList.remove(style.active); 53 | this.moreActions.style.height = '0px'; 54 | this.moreActions.style.transform = 'scaley(0)'; 55 | this.formElement.style.minHeight = '56px'; 56 | this.formElement.style.height = '56px'; 57 | } 58 | 59 | submitOnEnter(e) { 60 | if ((e && e.keyCode === 13) || e === 0) { 61 | this.createTask(e); 62 | } 63 | 64 | this.inputTask.style.height = this.inputTask.scrollHeight + 'px'; 65 | 66 | if (this.inputTask.value !== '') this.expandForm(); 67 | else this.contractForm(); 68 | } 69 | 70 | addGroup(e) { 71 | e.preventDefault(); 72 | 73 | this.props.stores.uiStore.openDialog('Add to a Group', ( 74 |
75 | 76 | {this.groups.map(group => ( 77 | 78 | ))} 79 | 80 | )); 81 | } 82 | 83 | setDueDate(e) { 84 | e.preventDefault(); 85 | if (!this.state.dueDate) this.props.stores.uiStore.datePicker.open(dueDate => this.setState({ dueDate })); 86 | else this.setState({ dueDate: null }); 87 | } 88 | 89 | setSelectedGroup(e) { 90 | this.setState({ selectedGroupColor: e.target.value }); 91 | } 92 | 93 | closeDialogOnEnter(e) { 94 | if ((e && e.keyCode === 13) || e === 0) { 95 | this.props.stores.uiStore.closeDialog(); 96 | } 97 | } 98 | 99 | constructor() { 100 | super(); 101 | 102 | this.state = null; 103 | 104 | this.groups = [ 105 | { 106 | id: 'orange', 107 | name: 'Shopping' 108 | }, 109 | { 110 | id: 'red', 111 | name: 'Web Development' 112 | }, 113 | { 114 | id: 'purple', 115 | name: 'School' 116 | }, 117 | { 118 | id: 'blue', 119 | name: 'Familiy' 120 | } 121 | ]; 122 | 123 | this.createTask = this.createTask.bind(this); 124 | this.expandForm = this.expandForm.bind(this); 125 | this.contractForm = this.contractForm.bind(this); 126 | this.addGroup = this.addGroup.bind(this); 127 | this.setDueDate = this.setDueDate.bind(this); 128 | this.setSelectedGroup = this.setSelectedGroup.bind(this); 129 | this.submitOnEnter = this.submitOnEnter.bind(this); 130 | this.closeDialogOnEnter = this.closeDialogOnEnter.bind(this); 131 | } 132 | 133 | componentDidMount() { 134 | this.inputTask.style.maxWidth = (this.formElement.clientWidth - 32) + 'px'; 135 | this.inputTask.style.minWidth = (this.formElement.clientWidth - 32) + 'px'; 136 | 137 | window.addEventListener('resize', () => { 138 | this.inputTask.style.maxWidth = (this.formElement.clientWidth - 32) + 'px'; 139 | this.inputTask.style.minWidth = (this.formElement.clientWidth - 32) + 'px'; 140 | }); 141 | } 142 | 143 | render() { 144 | 145 | return ( 146 |
this.formElement = el} onSubmit={this.createTask} autocomplete="off"> 147 |
this.colorIndicatorElement = el} class={style.colorIndicator} style={{ background: this.state.selectedGroupColor || '#78909c' }} /> 148 | 149 |