├── .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 |
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 |
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 |
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:
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 |