├── .gitignore ├── assets ├── screenshot.png ├── firebase_init_step_1.png └── firebase_init_step_2.png ├── .stylelintrc.json ├── firestore.indexes.json ├── public ├── src │ ├── css │ │ └── styles.css │ ├── models │ │ ├── user.js │ │ ├── project.js │ │ └── todo.js │ ├── modules │ │ ├── database.js │ │ └── doman.js │ └── index.js ├── 404.html └── dist │ ├── 404.html │ ├── index.html │ └── main.js ├── .eslintrc.json ├── webpack.config.js ├── firestore.rules ├── LICENSE ├── .github └── workflows │ └── linters.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | firebase-debug.log 3 | .firebaserc 4 | firebase.json -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abdelp/todo-list-js/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /assets/firebase_init_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abdelp/todo-list-js/HEAD/assets/firebase_init_step_1.png -------------------------------------------------------------------------------- /assets/firebase_init_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abdelp/todo-list-js/HEAD/assets/firebase_init_step_2.png -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss"], 4 | "rules": { 5 | "at-rule-no-unknown": null, 6 | "scss/at-rule-no-unknown": true 7 | }, 8 | "ignoreFiles": [ 9 | "build/**", 10 | "dist/**", 11 | "**/reset*.css", 12 | "**/bootstrap*.css" 13 | ] 14 | } -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [ 3 | { 4 | "collectionGroup": "projects", 5 | "queryScope": "COLLECTION", 6 | "fields": [ 7 | { 8 | "fieldPath": "userId", 9 | "order": "ASCENDING" 10 | }, 11 | { 12 | "fieldPath": "createdAt", 13 | "order": "DESCENDING" 14 | } 15 | ] 16 | } 17 | ], 18 | "fieldOverrides": [] 19 | } 20 | -------------------------------------------------------------------------------- /public/src/css/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Comfortaa', cursive; 3 | } 4 | 5 | .heading { 6 | font-family: 'Ranchers', cursive; 7 | font-size: 50px; 8 | } 9 | 10 | body { 11 | background-image: url("https://thumbs.dreamstime.com/b/stationery-background-school-tools-seamless-pattern-art-education-wallpaper-line-icons-pencil-pen-paintbrush-palette-169146367.jpg"); 12 | height: 100%; 13 | background-position: center; 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2018, 9 | "sourceType": "module" 10 | }, 11 | "extends": ["airbnb-base"], 12 | "rules": { 13 | "no-shadow": "off", 14 | "no-param-reassign": "off", 15 | "eol-last": "off", 16 | "arrow-parens": "off" 17 | }, 18 | "ignorePatterns": [ 19 | "dist/", 20 | "build/" 21 | ] 22 | } -------------------------------------------------------------------------------- /public/src/models/user.js: -------------------------------------------------------------------------------- 1 | import * as Database from '../modules/database'; 2 | 3 | const params = ({ userName, createdAt }) => ({ userName, createdAt }); 4 | 5 | const create = async (data) => { 6 | const collection = 'users'; 7 | let result; 8 | data.createdAt = Database.currentTimestamp(); 9 | 10 | try { 11 | result = await Database.add(collection, params(data)); 12 | localStorage.setItem('userId', result.id); 13 | } catch (error) { 14 | result = await error; 15 | } 16 | 17 | return result; 18 | }; 19 | 20 | export default create; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './public/src/index.js', 6 | output: { 7 | filename: 'main.js', 8 | path: path.resolve(__dirname, 'public/dist'), 9 | }, 10 | module: { 11 | rules: [{ 12 | test: /\.css$/, 13 | use: [ 14 | 'style-loader', 15 | 'css-loader', 16 | ], 17 | }, 18 | { 19 | test: /\.(png|svg|jpg|gif)$/, 20 | use: [ 21 | 'file-loader', 22 | ], 23 | }, 24 | { 25 | test: /\.(woff|woff2|eot|ttf|otf)$/, 26 | use: [ 27 | 'file-loader', 28 | ], 29 | }, 30 | ], 31 | }, 32 | }; -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | 5 | // This rule allows anyone with your database reference to view, edit, 6 | // and delete all data in your Firestore database. It is useful for getting 7 | // started, but it is configured to expire after 30 days because it 8 | // leaves your app open to attackers. At that time, all client 9 | // requests to your Firestore database will be denied. 10 | // 11 | // Make sure to write security rules for your app before that time, or else 12 | // all client requests to your Firestore database will be denied until you Update 13 | // your rules 14 | match /{document=**} { 15 | allow read, write: if request.time < timestamp.date(2020, 8, 19); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /public/src/models/project.js: -------------------------------------------------------------------------------- 1 | import * as Database from '../modules/database'; 2 | 3 | const params = ({ 4 | title, description, userId, createdAt, 5 | }) => ({ 6 | title, description, userId, createdAt, 7 | }); 8 | 9 | const create = async (data) => { 10 | const collection = 'projects'; 11 | let result; 12 | data.createdAt = Database.currentTimestamp(); 13 | 14 | try { 15 | result = await Database.add(collection, params(data)); 16 | } catch (error) { 17 | result = await error; 18 | } 19 | 20 | return result; 21 | }; 22 | 23 | const allProjects = async (userId) => { 24 | const collection = 'projects'; 25 | const projects = await Database.getCollection(collection, { params: [{ key: 'userId', sign: '==', value: userId }], orderBy: { field: 'createdAt', order: 'desc' } }); 26 | return projects; 27 | }; 28 | 29 | export { create, allProjects }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Abdel Omar Pérez Téllez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "12.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@6.8.x eslint-config-airbnb-base@14.1.x eslint-plugin-import@2.20.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/javascript/.eslintrc.json 21 | - name: ESLint Report 22 | run: npx eslint . 23 | stylelint: 24 | name: Stylelint 25 | runs-on: ubuntu-18.04 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v1 29 | with: 30 | node-version: "12.x" 31 | - name: Setup Stylelint 32 | run: | 33 | npm install --save-dev stylelint@13.3.x stylelint-scss@3.17.x stylelint-config-standard@20.0.x 34 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/javascript/.stylelintrc.json 35 | - name: Stylelint Report 36 | run: npx stylelint "**/*.{css,scss}" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list", 3 | "version": "1.0.0", 4 | "description": "A TO-DO application made with javascript", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack", 9 | "watch": "webpack --watch", 10 | "server": "webpack-dev-server --open" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/abdelp/todo-list-js.git" 15 | }, 16 | "keywords": [ 17 | "javascript" 18 | ], 19 | "author": "Elbie Moonga & Abdel Pérez", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/abdelp/todo-list-js/issues" 23 | }, 24 | "homepage": "https://github.com/abdelp/todo-list-js#readme", 25 | "devDependencies": { 26 | "autoprefixer": "^9.8.5", 27 | "css-loader": "^3.6.0", 28 | "eslint": "^6.8.0", 29 | "eslint-config-airbnb-base": "^14.1.0", 30 | "eslint-plugin-import": "^2.20.2", 31 | "exports-loader": "^1.1.0", 32 | "node-sass": "^4.14.1", 33 | "postcss-loader": "^3.0.0", 34 | "sass-loader": "^9.0.2", 35 | "style-loader": "^1.2.1", 36 | "stylelint": "^13.3.3", 37 | "stylelint-config-standard": "^20.0.0", 38 | "stylelint-csstree-validator": "^1.8.0", 39 | "stylelint-scss": "^3.17.2", 40 | "webpack": "^4.43.0", 41 | "webpack-cli": "^3.3.12", 42 | "webpack-dev-server": "^3.11.0" 43 | }, 44 | "dependencies": { 45 | "pubsub-js": "^1.8.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/src/models/todo.js: -------------------------------------------------------------------------------- 1 | import * as Database from '../modules/database'; 2 | 3 | const params = ({ 4 | title, description, dueDate, priority, 5 | }) => ({ 6 | title, description, dueDate, priority, 7 | }); 8 | 9 | const create = async (projectId, data) => { 10 | const collection = `projects/${projectId}/todos`; 11 | let result; 12 | try { 13 | result = await Database.add(collection, params(data)); 14 | } catch (error) { 15 | result = await error; 16 | } 17 | 18 | return result; 19 | }; 20 | 21 | const update = async (projectId, data) => { 22 | const collection = `projects/${projectId}/todos`; 23 | const { id: doc } = data; 24 | let result; 25 | try { 26 | result = await Database.edit(collection, doc, params(data)); 27 | } catch (error) { 28 | result = await error; 29 | } 30 | 31 | return result; 32 | }; 33 | 34 | const deleteTodo = async (projectId, docId) => { 35 | const collection = `projects/${projectId}/todos`; 36 | let result; 37 | 38 | try { 39 | result = await Database.deleteDoc(collection, docId); 40 | } catch (error) { 41 | result = await error; 42 | } 43 | 44 | return result; 45 | }; 46 | 47 | const allTodos = async (projectId) => { 48 | const collection = `projects/${projectId}/todos`; 49 | const todos = await Database.getCollection(collection); 50 | return todos; 51 | }; 52 | 53 | const where = async (projectId, conditions) => { 54 | const collection = `projects/${projectId}/todos`; 55 | let result; 56 | 57 | try { 58 | result = await Database.getCollection(collection, conditions); 59 | } catch (error) { 60 | result = await error; 61 | } 62 | 63 | return result; 64 | }; 65 | 66 | export { 67 | create, update, allTodos, deleteTodo, where, 68 | }; -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /public/dist/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Todo List Application 2 | 3 | > This project consists of building a todo list application with Javascript and firebase hosting. 4 | 5 | ![screenshot](./assets/screenshot.png) 6 | 7 | ## Built With 8 | 9 | - Javascript 10 | - Firebase 11 | - Webpack 12 | - HTML5 13 | - CSS 14 | - Bootstrap 4.5 15 | - ESLint 16 | - Stylelint 17 | 18 | ## Usage 19 | 20 | - To start the application follow the link given in the live demo. 21 | - A default project is assigned to you via a auto generated username. 22 | - Add new projects. 23 | - Add new todos within those projects you create 24 | 25 | ## Live Demo 26 | 27 | [Live Demo Link](https://todo-list-41950.web.app/) 28 | 29 | 30 | ## Requirements 31 | 32 | - Compatible Web browser (Chrome, Mozilla, IE, Safari) 33 | 34 | ## Installation 35 | 36 | ### Prerequisite 37 | 38 | - Create a firebase [account](https://console.firebase.google.com/) 39 | - Install [firebase CLI](https://firebase.google.com/docs/cli) 40 | - Install [Node](https://nodejs.org/en/) 41 | 42 | ### Steps 43 | 44 | From the command line/terminal clone the repository: 45 | 46 | ``` 47 | $ git clone https://github.com/abdelp/todo-list-js.git 48 | ``` 49 | 50 | Initialize the firebase hosting service 51 | 52 | ``` 53 | $ firebase init 54 | ``` 55 | 56 | Select firestore and hosting features: 57 | 58 | ![features](./assets/firebase_init_step_1.png) 59 | 60 | Select public/dist as the public directory: 61 | 62 | ![public](./assets/firebase_init_step_2.png) 63 | 64 | ## Deploy 65 | 66 | To deploy on your local environment run: 67 | 68 | ``` 69 | $ firebase serve 70 | ``` 71 | 72 | To deploy to your firebase production environment: 73 | 74 | ``` 75 | $ firebase deploy 76 | ``` 77 | 78 | ## Authors 79 | 80 | 👤 **Abdel Pérez** 81 | 82 | - Github: [@abdelp](https://github.com/abdelp/) 83 | - Twitter: [@AbdelPerez11](https://twitter.com/abdelperez11) 84 | - Linkedin: [Abdel Pérez](https://www.linkedin.com/in/abdel-perez/) 85 | 86 | 87 | 👤 **Elbie Moonga** 88 | - GitHub: [@Elbie-Em](https://github.com/Elbie-em) 89 | - Twitter: [ElbieEm](https://twitter.com/ElbieEm) 90 | - LinkedIn: [elbie-moonga](https://www.linkedin.com/in/elbiemoonga/) 91 | 92 | ## 🤝 Contributing 93 | 94 | Contributions, issues and feature requests are welcome! 95 | 96 | Feel free to check the [issues page](https://github.com/abdelp/todo-list-js/issues). 97 | 98 | ## Show your support 99 | 100 | Give a ⭐️ if you like this project! 101 | 102 | ## 📝 License 103 | 104 | This project is [MIT](lic.url) licensed. -------------------------------------------------------------------------------- /public/src/modules/database.js: -------------------------------------------------------------------------------- 1 | const firestore = firebase.firestore(); // eslint-disable-line no-undef 2 | 3 | const add = async (collection, data) => { 4 | const collRef = firestore.collection(collection); 5 | let result; 6 | 7 | try { 8 | result = await collRef.add(data); 9 | } catch (error) { 10 | result = await error; 11 | } 12 | 13 | return result; 14 | }; 15 | 16 | const edit = async (collection, doc, data) => { 17 | const docRef = firestore.collection(collection).doc(doc); 18 | let result; 19 | 20 | try { 21 | result = await docRef.update(data); 22 | } catch (error) { 23 | result = await error; 24 | } 25 | 26 | return result; 27 | }; 28 | 29 | const deleteDoc = async (collection, doc) => { 30 | const docRef = firestore.collection(collection).doc(doc); 31 | let result; 32 | 33 | try { 34 | result = await docRef.delete(); 35 | } catch (error) { 36 | result = await error; 37 | } 38 | 39 | return result; 40 | }; 41 | 42 | const setCurrentProject = (projectId) => { 43 | localStorage.setItem('currentProject', projectId); 44 | }; 45 | 46 | const getCurrentProject = () => localStorage.getItem('currentProject'); 47 | 48 | const getDoc = async (collection, queryProps = {}) => { 49 | const { 50 | params, 51 | orderBy, 52 | doc, 53 | } = queryProps; 54 | let collectionRef = firestore.collection(collection); 55 | 56 | if (params) { 57 | params.forEach(param => { 58 | collectionRef = collectionRef.where(param.key, param.sign, param.value); 59 | }); 60 | } 61 | if (orderBy) { 62 | collectionRef = collectionRef.orderBy(orderBy.field, orderBy.order); 63 | } 64 | if (doc) { 65 | collectionRef = collectionRef.doc(doc); 66 | } 67 | 68 | let result; 69 | 70 | try { 71 | const rDoc = await collectionRef.get(); 72 | result = { 73 | id: rDoc.id, 74 | ...rDoc.data(), 75 | }; 76 | } catch (error) { 77 | result = await error; 78 | } 79 | 80 | return result; 81 | }; 82 | 83 | const getCollection = async (collection, queryProps = {}) => { 84 | const { 85 | params, 86 | orderBy, 87 | } = queryProps; 88 | let collectionRef = firestore.collection(collection); 89 | 90 | if (params) { 91 | params.forEach(param => { 92 | collectionRef = collectionRef.where(param.key, param.sign, param.value); 93 | }); 94 | } 95 | if (orderBy) { 96 | collectionRef = collectionRef.orderBy(orderBy.field, orderBy.order); 97 | } 98 | 99 | let result = []; 100 | 101 | try { 102 | const docs = await collectionRef.get(); 103 | 104 | docs.forEach(doc => { 105 | result.push({ 106 | id: doc.id, 107 | ...doc.data(), 108 | }); 109 | }); 110 | } catch (error) { 111 | result = await error; 112 | } 113 | 114 | return result; 115 | }; 116 | 117 | const getDefaultProject = async (userId) => { 118 | let result; 119 | 120 | try { 121 | const params = [{ 122 | key: 'title', 123 | sign: '==', 124 | value: 'default', 125 | }, 126 | { 127 | key: 'userId', 128 | sign: '==', 129 | value: userId, 130 | }, 131 | ]; 132 | result = await getDoc('projects', params); 133 | } catch (error) { 134 | result = await error; 135 | } 136 | 137 | return result[0]; 138 | }; 139 | 140 | const getUserId = () => localStorage.getItem('userId'); 141 | 142 | const currentTimestamp = () => { 143 | firebase.firestore.FieldValue.serverTimestamp(); // eslint-disable-line no-undef 144 | }; 145 | 146 | export { 147 | add, 148 | edit, 149 | getDoc, 150 | deleteDoc, 151 | getCollection, 152 | getUserId, 153 | setCurrentProject, 154 | getDefaultProject, 155 | currentTimestamp, 156 | getCurrentProject, 157 | }; -------------------------------------------------------------------------------- /public/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jquery */ 2 | import PubSub from 'pubsub-js'; 3 | import * as Doman from './modules/doman'; 4 | import * as Database from './modules/database'; 5 | import * as Todo from './models/todo'; 6 | import * as Project from './models/project'; 7 | import * as User from './models/user'; 8 | import './css/styles.css'; 9 | 10 | const addProject = () => { 11 | const formId = 'project-form'; 12 | const data = Doman.getFormValues(formId); 13 | const userId = Database.getUserId(); 14 | data.userId = userId; 15 | Project.create(data); 16 | Doman.cleanForm(formId); 17 | Doman.hideModal('project-modal'); 18 | PubSub.publish('LOAD PROJECTS'); 19 | }; 20 | 21 | Doman.assignBtn('add-project', addProject); 22 | 23 | $('#todo-modal').on('hidden.bs.modal', () => { 24 | Doman.cleanForm('todo-form'); 25 | }); 26 | 27 | const getCurrentDate = () => { 28 | const date = new Date(); 29 | const dateTimeFormat = new Intl.DateTimeFormat('en', { 30 | year: 'numeric', 31 | month: '2-digit', 32 | day: '2-digit', 33 | }); 34 | const [{ 35 | value: month, 36 | }, , { 37 | value: day, 38 | }, , { 39 | value: year, 40 | }] = dateTimeFormat.formatToParts(date); 41 | const currentDate = `${year}-${month}-${day}`; 42 | return currentDate; 43 | }; 44 | 45 | const loadTodos = async (msg, condition, projectId) => { 46 | let sign; 47 | const currentDate = getCurrentDate(); 48 | let container; 49 | 50 | if (condition === 'today') { 51 | sign = '=='; 52 | container = 'today-todo-list'; 53 | } else if (condition === 'upcoming') { 54 | sign = '>'; 55 | container = 'upcoming-todo-list'; 56 | } else if (condition === 'completed') { 57 | sign = '<'; 58 | container = 'completed-todo-list'; 59 | } 60 | 61 | const conditions = { 62 | params: [{ 63 | key: 'dueDate', 64 | sign, 65 | value: currentDate, 66 | }], 67 | }; 68 | 69 | const todos = await Todo.where(projectId, conditions); 70 | const todoCollapses = []; 71 | 72 | todos.forEach(todo => { 73 | const deleteHandler = () => { 74 | Todo.deleteTodo(projectId, todo.id); 75 | PubSub.publish('LOAD TODOS', projectId); 76 | Doman.hideModal('confirm-modal'); 77 | }; 78 | 79 | const data = { 80 | id: todo.id, 81 | innerText: todo.title, 82 | description: todo.description, 83 | dueDate: todo.dueDate, 84 | priority: todo.priority, 85 | deleteButton: { 86 | onclick: () => Doman.showConfirmModal(deleteHandler), 87 | }, 88 | }; 89 | 90 | const todoCollapse = Doman.createCollapse(data); 91 | todoCollapses.push(todoCollapse); 92 | }); 93 | 94 | const todoList = Doman.createList(todoCollapses); 95 | 96 | Doman.cleanElement(container); 97 | Doman.addChild(container, todoList); 98 | }; 99 | 100 | const addTodo = () => { 101 | const data = Doman.getFormValues('todo-form'); 102 | const currentProject = Database.getCurrentProject(); 103 | if (!data.id) { 104 | Todo.create(currentProject, data) 105 | .then(() => { 106 | Doman.cleanForm('todo-form'); 107 | Doman.hideModal('todo-modal'); 108 | PubSub.publish('LOAD TODOS', 'today', currentProject); 109 | }); 110 | } else { 111 | Todo.update(currentProject, data) 112 | .then(() => { 113 | Doman.cleanForm('todo-form'); 114 | Doman.hideModal('todo-modal'); 115 | PubSub.publish('LOAD TODOS', 'today', currentProject); 116 | }); 117 | } 118 | }; 119 | 120 | Doman.assignBtn('add-todo', addTodo); 121 | 122 | const loadProjects = () => { 123 | const userId = Database.getUserId(); 124 | Doman.cleanElement('projects-list'); 125 | Project.allProjects(userId) 126 | .then(result => { 127 | const onclickHandler = () => { 128 | Database.setCurrentProject(this.id); 129 | Doman.setTitle(this.innerHTML); 130 | loadTodos('', 'today', this.id); 131 | }; 132 | 133 | const projectsButtons = result.map(item => Doman.createButton({ 134 | id: item.id, 135 | innerText: item.title, 136 | color: 'info', 137 | onclick: onclickHandler, 138 | })); 139 | 140 | const list = Doman.createList(projectsButtons); 141 | Doman.addChild('projects-list', list); 142 | }); 143 | }; 144 | 145 | const userId = Database.getUserId(); 146 | 147 | if (!userId) { 148 | User.create({ 149 | userName: 'test', 150 | }) 151 | .then(user => { 152 | const data = { 153 | title: 'Default', 154 | description: 'This is the default project for your application', 155 | userId: user.id, 156 | }; 157 | Project.create(data) 158 | .then(project => { 159 | Database.setCurrentProject(project.id); 160 | Doman.setTitle(data.title); 161 | loadProjects(); 162 | }); 163 | }); 164 | } else { 165 | Database.getDoc('projects', { 166 | doc: Database.getCurrentProject(), 167 | }) 168 | .then(doc => { 169 | Doman.setTitle(doc.title); 170 | loadTodos('', 'today', doc.id); 171 | }); 172 | } 173 | 174 | PubSub.subscribe('LOAD PROJECTS', loadProjects); 175 | PubSub.subscribe('LOAD TODOS', loadTodos); 176 | 177 | loadProjects(); 178 | 179 | Doman.assignBtn('completed-todos-btn', () => loadTodos('', 'completed', Database.getCurrentProject())); 180 | Doman.assignBtn('upcoming-todos-btn', () => loadTodos('', 'upcoming', Database.getCurrentProject())); 181 | Doman.assignBtn('today-todos-btn', () => loadTodos('', 'today', Database.getCurrentProject())); -------------------------------------------------------------------------------- /public/src/modules/doman.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jquery */ 2 | const getFormValues = formId => { 3 | const { 4 | elements, 5 | } = document.getElementById(formId); 6 | const obj = {}; 7 | for (let i = 0; i < elements.length; i += 1) { 8 | const item = elements.item(i); 9 | obj[item.name] = item.value; 10 | } 11 | 12 | return obj; 13 | }; 14 | 15 | const cleanForm = formId => { 16 | const form = document.getElementById(formId); 17 | form.reset(); 18 | }; 19 | 20 | const hideModal = modalId => { 21 | $(`#${modalId}`).modal('hide'); 22 | }; 23 | 24 | const createList = (list) => { 25 | const ul = document.createElement('ul'); 26 | ul.className = 'list-group mt-3'; 27 | list.forEach(item => { 28 | const li = document.createElement('li'); 29 | li.className = 'list-group-item border-0'; 30 | li.id = item.id; 31 | li.appendChild(item); 32 | ul.appendChild(li); 33 | }); 34 | 35 | return ul; 36 | }; 37 | 38 | const setTitle = (title) => { 39 | const elem = document.getElementById('project-name'); 40 | elem.innerHTML = title; 41 | }; 42 | 43 | const cleanElement = (containerId) => { 44 | const container = document.getElementById(containerId); 45 | container.innerHTML = ''; 46 | }; 47 | 48 | const addChild = (containerId, element) => { 49 | const container = document.getElementById(containerId); 50 | container.appendChild(element); 51 | }; 52 | 53 | const createButton = (params) => { 54 | const { 55 | id, 56 | color = 'primary', 57 | innerText, 58 | onclick, 59 | } = params; 60 | 61 | const btn = document.createElement('button'); 62 | btn.id = id; 63 | btn.className = `btn btn-${color} w-100 mb-1`; 64 | btn.innerText = innerText; 65 | btn.onclick = onclick; 66 | btn.type = 'button'; 67 | return btn; 68 | }; 69 | 70 | const createBadge = (priority) => { 71 | const badge = document.createElement('span'); 72 | if (priority === '1') { 73 | badge.className = 'badge badge-danger h-75'; 74 | badge.innerText = 'High'; 75 | } else if (priority === '2') { 76 | badge.className = 'badge badge-success h-75'; 77 | badge.innerHTML = 'Medium'; 78 | } else if (priority === '3') { 79 | badge.className = 'badge badge-warning h-75'; 80 | badge.innerHTML = 'Low'; 81 | } 82 | 83 | return badge; 84 | }; 85 | 86 | const select = (selectId, optionValToSelect) => { 87 | const selectElement = document.getElementById(selectId); 88 | const selectOptions = selectElement.options; 89 | let cont = true; 90 | let j = 0; 91 | 92 | while (cont) { 93 | const opt = selectOptions[j]; 94 | if (opt.value === optionValToSelect) { 95 | selectElement.selectedIndex = j; 96 | cont = false; 97 | } 98 | j += 1; 99 | } 100 | }; 101 | 102 | const openEditModal = (modalId, todo) => { 103 | const form = document.getElementById('todo-modal').querySelector('form'); 104 | const hiddenInput = form.querySelector('#id'); 105 | hiddenInput.setAttribute('value', todo.id); 106 | form.querySelector('#title').value = todo.innerText; 107 | form.querySelector('#description').value = todo.description; 108 | form.querySelector('#dueDate').value = todo.dueDate; 109 | select('priority', todo.priority); 110 | $('#todo-modal').modal('show'); 111 | }; 112 | 113 | const createCollapse = (element) => { 114 | const container = document.createElement('div'); 115 | const collapseBtn = document.createElement('button'); 116 | collapseBtn.className = 'btn btn-secondary w-100 mt-2'; 117 | collapseBtn.type = 'button'; 118 | collapseBtn.setAttribute('data-toggle', 'collapse'); 119 | const collapseId = `t-${element.id}`; 120 | collapseBtn.setAttribute('data-target', `#${collapseId}`); 121 | collapseBtn.setAttribute('aria-expanded', 'false'); 122 | collapseBtn.setAttribute('aria-controls', collapseId); 123 | collapseBtn.innerText = element.innerText; 124 | 125 | const collapse = document.createElement('div'); 126 | collapse.id = collapseId; 127 | collapse.className = 'collapse'; 128 | const collapseBody = document.createElement('div'); 129 | collapseBody.className = 'card card-body'; 130 | 131 | const todoTop = document.createElement('div'); 132 | todoTop.className = 'd-flex flex-row justify-content-between'; 133 | 134 | const todoDate = document.createElement('h6'); 135 | todoDate.innerHTML = `Date: ${element.dueDate}`; 136 | 137 | const todoPriority = createBadge(element.priority); 138 | 139 | todoTop.appendChild(todoDate); 140 | todoTop.appendChild(todoPriority); 141 | 142 | const todoBody = document.createElement('div'); 143 | const todoDescription = document.createElement('p'); 144 | todoDescription.innerHTML = `Details
${element.description}`; 145 | 146 | todoBody.appendChild(todoDescription); 147 | 148 | collapseBody.appendChild(todoTop); 149 | collapseBody.appendChild(todoBody); 150 | 151 | collapse.appendChild(collapseBody); 152 | 153 | const todoBottom = document.createElement('div'); 154 | todoBottom.className = 'text-right'; 155 | 156 | const editBtn = createButton({ 157 | id: `edit-btn-${collapseId}`, 158 | color: 'success', 159 | onclick: () => openEditModal('todo-modal', element), 160 | innerText: 'Edit', 161 | }); 162 | todoBottom.appendChild(editBtn); 163 | 164 | const deleteBtn = createButton({ 165 | id: `dlt-btn-${collapseId}`, 166 | color: 'danger', 167 | onclick: element.deleteButton.onclick, 168 | innerText: 'Delete', 169 | }); 170 | todoBottom.appendChild(deleteBtn); 171 | 172 | todoBody.appendChild(todoBottom); 173 | 174 | container.appendChild(collapseBtn); 175 | container.appendChild(collapse); 176 | 177 | return container; 178 | }; 179 | 180 | const showConfirmModal = (deleteHandler) => { 181 | const deleteBtn = document.getElementById('confirm-btn'); 182 | deleteBtn.onclick = deleteHandler; 183 | $('#confirm-modal').modal('show'); 184 | }; 185 | 186 | const assignBtn = (btnId, onclickHandler) => { 187 | const btnElement = document.getElementById(btnId); 188 | btnElement.onclick = onclickHandler; 189 | }; 190 | 191 | export { 192 | getFormValues, 193 | cleanForm, 194 | hideModal, 195 | createList, 196 | addChild, 197 | cleanElement, 198 | setTitle, 199 | createButton, 200 | createCollapse, 201 | showConfirmModal, 202 | assignBtn, 203 | }; -------------------------------------------------------------------------------- /public/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | todo.app 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

TODO APP

23 | 42 | 82 | 110 | 111 |
112 |
113 | 116 |
117 |
118 |
119 |
120 |
121 |

Project:

122 | 125 |
126 | 129 |
130 |
131 |
132 | 133 |
134 |
135 |
136 | 139 |
140 |
141 |
142 | 143 |
144 |
145 |
146 | 149 |
150 |
151 |
152 | 153 |
154 |
155 |
156 |
157 |
158 |
159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /public/dist/main.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = ""; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./public/src/index.js"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./node_modules/css-loader/dist/cjs.js!./public/src/css/styles.css": 90 | /*!*************************************************************************!*\ 91 | !*** ./node_modules/css-loader/dist/cjs.js!./public/src/css/styles.css ***! 92 | \*************************************************************************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports, __webpack_require__) { 95 | 96 | eval("// Imports\nvar ___CSS_LOADER_API_IMPORT___ = __webpack_require__(/*! ../../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\nexports = ___CSS_LOADER_API_IMPORT___(false);\n// Module\nexports.push([module.i, \"* {\\r\\n font-family: 'Comfortaa', cursive;\\r\\n}\\r\\n\\r\\n.heading {\\r\\n font-family: 'Ranchers', cursive;\\r\\n font-size: 50px;\\r\\n}\\r\\n\\r\\nbody {\\r\\n background-image: url(\\\"https://thumbs.dreamstime.com/b/stationery-background-school-tools-seamless-pattern-art-education-wallpaper-line-icons-pencil-pen-paintbrush-palette-169146367.jpg\\\");\\r\\n height: 100%;\\r\\n background-position: center;\\r\\n}\\r\\n\", \"\"]);\n// Exports\nmodule.exports = exports;\n\n\n//# sourceURL=webpack:///./public/src/css/styles.css?./node_modules/css-loader/dist/cjs.js"); 97 | 98 | /***/ }), 99 | 100 | /***/ "./node_modules/css-loader/dist/runtime/api.js": 101 | /*!*****************************************************!*\ 102 | !*** ./node_modules/css-loader/dist/runtime/api.js ***! 103 | \*****************************************************/ 104 | /*! no static exports found */ 105 | /***/ (function(module, exports, __webpack_require__) { 106 | 107 | "use strict"; 108 | eval("\n\n/*\n MIT License http://www.opensource.org/licenses/mit-license.php\n Author Tobias Koppers @sokra\n*/\n// css base code, injected by the css-loader\n// eslint-disable-next-line func-names\nmodule.exports = function (useSourceMap) {\n var list = []; // return the list of modules as css string\n\n list.toString = function toString() {\n return this.map(function (item) {\n var content = cssWithMappingToString(item, useSourceMap);\n\n if (item[2]) {\n return \"@media \".concat(item[2], \" {\").concat(content, \"}\");\n }\n\n return content;\n }).join('');\n }; // import a list of modules into the list\n // eslint-disable-next-line func-names\n\n\n list.i = function (modules, mediaQuery, dedupe) {\n if (typeof modules === 'string') {\n // eslint-disable-next-line no-param-reassign\n modules = [[null, modules, '']];\n }\n\n var alreadyImportedModules = {};\n\n if (dedupe) {\n for (var i = 0; i < this.length; i++) {\n // eslint-disable-next-line prefer-destructuring\n var id = this[i][0];\n\n if (id != null) {\n alreadyImportedModules[id] = true;\n }\n }\n }\n\n for (var _i = 0; _i < modules.length; _i++) {\n var item = [].concat(modules[_i]);\n\n if (dedupe && alreadyImportedModules[item[0]]) {\n // eslint-disable-next-line no-continue\n continue;\n }\n\n if (mediaQuery) {\n if (!item[2]) {\n item[2] = mediaQuery;\n } else {\n item[2] = \"\".concat(mediaQuery, \" and \").concat(item[2]);\n }\n }\n\n list.push(item);\n }\n };\n\n return list;\n};\n\nfunction cssWithMappingToString(item, useSourceMap) {\n var content = item[1] || ''; // eslint-disable-next-line prefer-destructuring\n\n var cssMapping = item[3];\n\n if (!cssMapping) {\n return content;\n }\n\n if (useSourceMap && typeof btoa === 'function') {\n var sourceMapping = toComment(cssMapping);\n var sourceURLs = cssMapping.sources.map(function (source) {\n return \"/*# sourceURL=\".concat(cssMapping.sourceRoot || '').concat(source, \" */\");\n });\n return [content].concat(sourceURLs).concat([sourceMapping]).join('\\n');\n }\n\n return [content].join('\\n');\n} // Adapted from convert-source-map (MIT)\n\n\nfunction toComment(sourceMap) {\n // eslint-disable-next-line no-undef\n var base64 = btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap))));\n var data = \"sourceMappingURL=data:application/json;charset=utf-8;base64,\".concat(base64);\n return \"/*# \".concat(data, \" */\");\n}\n\n//# sourceURL=webpack:///./node_modules/css-loader/dist/runtime/api.js?"); 109 | 110 | /***/ }), 111 | 112 | /***/ "./node_modules/pubsub-js/src/pubsub.js": 113 | /*!**********************************************!*\ 114 | !*** ./node_modules/pubsub-js/src/pubsub.js ***! 115 | \**********************************************/ 116 | /*! no static exports found */ 117 | /***/ (function(module, exports, __webpack_require__) { 118 | 119 | eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * Copyright (c) 2010,2011,2012,2013,2014 Morgan Roderick http://roderick.dk\n * License: MIT - http://mrgnrdrck.mit-license.org\n *\n * https://github.com/mroderick/PubSubJS\n */\n\n(function (root, factory){\n 'use strict';\n\n var PubSub = {};\n root.PubSub = PubSub;\n\n var define = root.define;\n\n factory(PubSub);\n\n // AMD support\n if (typeof define === 'function' && define.amd){\n define(function() { return PubSub; });\n\n // CommonJS and Node.js module support\n } else if (true){\n if (module !== undefined && module.exports) {\n exports = module.exports = PubSub; // Node.js specific `module.exports`\n }\n exports.PubSub = PubSub; // CommonJS module 1.1.1 spec\n module.exports = exports = PubSub; // CommonJS\n }\n\n}(( typeof window === 'object' && window ) || this, function (PubSub){\n 'use strict';\n\n var messages = {},\n lastUid = -1;\n\n function hasKeys(obj){\n var key;\n\n for (key in obj){\n if ( obj.hasOwnProperty(key) ){\n return true;\n }\n }\n return false;\n }\n\n /**\n * Returns a function that throws the passed exception, for use as argument for setTimeout\n * @alias throwException\n * @function\n * @param { Object } ex An Error object\n */\n function throwException( ex ){\n return function reThrowException(){\n throw ex;\n };\n }\n\n function callSubscriberWithDelayedExceptions( subscriber, message, data ){\n try {\n subscriber( message, data );\n } catch( ex ){\n setTimeout( throwException( ex ), 0);\n }\n }\n\n function callSubscriberWithImmediateExceptions( subscriber, message, data ){\n subscriber( message, data );\n }\n\n function deliverMessage( originalMessage, matchedMessage, data, immediateExceptions ){\n var subscribers = messages[matchedMessage],\n callSubscriber = immediateExceptions ? callSubscriberWithImmediateExceptions : callSubscriberWithDelayedExceptions,\n s;\n\n if ( !messages.hasOwnProperty( matchedMessage ) ) {\n return;\n }\n\n for (s in subscribers){\n if ( subscribers.hasOwnProperty(s)){\n callSubscriber( subscribers[s], originalMessage, data );\n }\n }\n }\n\n function createDeliveryFunction( message, data, immediateExceptions ){\n return function deliverNamespaced(){\n var topic = String( message ),\n position = topic.lastIndexOf( '.' );\n\n // deliver the message as it is now\n deliverMessage(message, message, data, immediateExceptions);\n\n // trim the hierarchy and deliver message to each level\n while( position !== -1 ){\n topic = topic.substr( 0, position );\n position = topic.lastIndexOf('.');\n deliverMessage( message, topic, data, immediateExceptions );\n }\n };\n }\n\n function messageHasSubscribers( message ){\n var topic = String( message ),\n found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic])),\n position = topic.lastIndexOf( '.' );\n\n while ( !found && position !== -1 ){\n topic = topic.substr( 0, position );\n position = topic.lastIndexOf( '.' );\n found = Boolean(messages.hasOwnProperty( topic ) && hasKeys(messages[topic]));\n }\n\n return found;\n }\n\n function publish( message, data, sync, immediateExceptions ){\n message = (typeof message === 'symbol') ? message.toString() : message;\n\n var deliver = createDeliveryFunction( message, data, immediateExceptions ),\n hasSubscribers = messageHasSubscribers( message );\n\n if ( !hasSubscribers ){\n return false;\n }\n\n if ( sync === true ){\n deliver();\n } else {\n setTimeout( deliver, 0 );\n }\n return true;\n }\n\n /**\n * Publishes the message, passing the data to it's subscribers\n * @function\n * @alias publish\n * @param { String } message The message to publish\n * @param {} data The data to pass to subscribers\n * @return { Boolean }\n */\n PubSub.publish = function( message, data ){\n return publish( message, data, false, PubSub.immediateExceptions );\n };\n\n /**\n * Publishes the message synchronously, passing the data to it's subscribers\n * @function\n * @alias publishSync\n * @param { String } message The message to publish\n * @param {} data The data to pass to subscribers\n * @return { Boolean }\n */\n PubSub.publishSync = function( message, data ){\n return publish( message, data, true, PubSub.immediateExceptions );\n };\n\n /**\n * Subscribes the passed function to the passed message. Every returned token is unique and should be stored if you need to unsubscribe\n * @function\n * @alias subscribe\n * @param { String } message The message to subscribe to\n * @param { Function } func The function to call when a new message is published\n * @return { String }\n */\n PubSub.subscribe = function( message, func ){\n if ( typeof func !== 'function'){\n return false;\n }\n\n message = (typeof message === 'symbol') ? message.toString() : message;\n\n // message is not registered yet\n if ( !messages.hasOwnProperty( message ) ){\n messages[message] = {};\n }\n\n // forcing token as String, to allow for future expansions without breaking usage\n // and allow for easy use as key names for the 'messages' object\n var token = 'uid_' + String(++lastUid);\n messages[message][token] = func;\n \n // return token for unsubscribing\n return token;\n };\n\n /**\n * Subscribes the passed function to the passed message once\n * @function\n * @alias subscribeOnce\n * @param { String } message The message to subscribe to\n * @param { Function } func The function to call when a new message is published\n * @return { PubSub }\n */\n PubSub.subscribeOnce = function( message, func ){\n var token = PubSub.subscribe( message, function(){\n // before func apply, unsubscribe message\n PubSub.unsubscribe( token );\n func.apply( this, arguments );\n });\n return PubSub;\n };\n\n /**\n * Clears all subscriptions\n * @function\n * @public\n * @alias clearAllSubscriptions\n */\n PubSub.clearAllSubscriptions = function clearAllSubscriptions(){\n messages = {};\n };\n\n /**\n * Clear subscriptions by the topic\n * @function\n * @public\n * @alias clearAllSubscriptions\n * @return { int }\n */\n PubSub.clearSubscriptions = function clearSubscriptions(topic){\n var m;\n for (m in messages){\n if (messages.hasOwnProperty(m) && m.indexOf(topic) === 0){\n delete messages[m];\n }\n }\n };\n\n /** \n Count subscriptions by the topic\n * @function\n * @public\n * @alias countSubscriptions\n * @return { Array }\n */\n PubSub.countSubscriptions = function countSubscriptions(topic){\n var m;\n var count = 0;\n for (m in messages){\n if (messages.hasOwnProperty(m) && m.indexOf(topic) === 0){\n count++;\n }\n }\n return count;\n };\n\n \n /** \n Gets subscriptions by the topic\n * @function\n * @public\n * @alias getSubscriptions\n */\n PubSub.getSubscriptions = function getSubscriptions(topic){\n var m;\n var list = [];\n for (m in messages){\n if (messages.hasOwnProperty(m) && m.indexOf(topic) === 0){\n list.push(m);\n }\n }\n return list;\n };\n\n /**\n * Removes subscriptions\n *\n * - When passed a token, removes a specific subscription.\n *\n\t * - When passed a function, removes all subscriptions for that function\n *\n\t * - When passed a topic, removes all subscriptions for that topic (hierarchy)\n * @function\n * @public\n * @alias subscribeOnce\n * @param { String | Function } value A token, function or topic to unsubscribe from\n * @example // Unsubscribing with a token\n * var token = PubSub.subscribe('mytopic', myFunc);\n * PubSub.unsubscribe(token);\n * @example // Unsubscribing with a function\n * PubSub.unsubscribe(myFunc);\n * @example // Unsubscribing from a topic\n * PubSub.unsubscribe('mytopic');\n */\n PubSub.unsubscribe = function(value){\n var descendantTopicExists = function(topic) {\n var m;\n for ( m in messages ){\n if ( messages.hasOwnProperty(m) && m.indexOf(topic) === 0 ){\n // a descendant of the topic exists:\n return true;\n }\n }\n\n return false;\n },\n isTopic = typeof value === 'string' && ( messages.hasOwnProperty(value) || descendantTopicExists(value) ),\n isToken = !isTopic && typeof value === 'string',\n isFunction = typeof value === 'function',\n result = false,\n m, message, t;\n\n if (isTopic){\n PubSub.clearSubscriptions(value);\n return;\n }\n\n for ( m in messages ){\n if ( messages.hasOwnProperty( m ) ){\n message = messages[m];\n\n if ( isToken && message[value] ){\n delete message[value];\n result = value;\n // tokens are unique, so we can just stop here\n break;\n }\n\n if (isFunction) {\n for ( t in message ){\n if (message.hasOwnProperty(t) && message[t] === value){\n delete message[t];\n result = true;\n }\n }\n }\n }\n }\n\n return result;\n };\n}));\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../webpack/buildin/module.js */ \"./node_modules/webpack/buildin/module.js\")(module)))\n\n//# sourceURL=webpack:///./node_modules/pubsub-js/src/pubsub.js?"); 120 | 121 | /***/ }), 122 | 123 | /***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js": 124 | /*!****************************************************************************!*\ 125 | !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***! 126 | \****************************************************************************/ 127 | /*! no static exports found */ 128 | /***/ (function(module, exports, __webpack_require__) { 129 | 130 | "use strict"; 131 | eval("\n\nvar isOldIE = function isOldIE() {\n var memo;\n return function memorize() {\n if (typeof memo === 'undefined') {\n // Test for IE <= 9 as proposed by Browserhacks\n // @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805\n // Tests for existence of standard globals is to allow style-loader\n // to operate correctly into non-standard environments\n // @see https://github.com/webpack-contrib/style-loader/issues/177\n memo = Boolean(window && document && document.all && !window.atob);\n }\n\n return memo;\n };\n}();\n\nvar getTarget = function getTarget() {\n var memo = {};\n return function memorize(target) {\n if (typeof memo[target] === 'undefined') {\n var styleTarget = document.querySelector(target); // Special case to return head of iframe instead of iframe itself\n\n if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {\n try {\n // This will throw an exception if access to iframe is blocked\n // due to cross-origin restrictions\n styleTarget = styleTarget.contentDocument.head;\n } catch (e) {\n // istanbul ignore next\n styleTarget = null;\n }\n }\n\n memo[target] = styleTarget;\n }\n\n return memo[target];\n };\n}();\n\nvar stylesInDom = [];\n\nfunction getIndexByIdentifier(identifier) {\n var result = -1;\n\n for (var i = 0; i < stylesInDom.length; i++) {\n if (stylesInDom[i].identifier === identifier) {\n result = i;\n break;\n }\n }\n\n return result;\n}\n\nfunction modulesToDom(list, options) {\n var idCountMap = {};\n var identifiers = [];\n\n for (var i = 0; i < list.length; i++) {\n var item = list[i];\n var id = options.base ? item[0] + options.base : item[0];\n var count = idCountMap[id] || 0;\n var identifier = \"\".concat(id, \" \").concat(count);\n idCountMap[id] = count + 1;\n var index = getIndexByIdentifier(identifier);\n var obj = {\n css: item[1],\n media: item[2],\n sourceMap: item[3]\n };\n\n if (index !== -1) {\n stylesInDom[index].references++;\n stylesInDom[index].updater(obj);\n } else {\n stylesInDom.push({\n identifier: identifier,\n updater: addStyle(obj, options),\n references: 1\n });\n }\n\n identifiers.push(identifier);\n }\n\n return identifiers;\n}\n\nfunction insertStyleElement(options) {\n var style = document.createElement('style');\n var attributes = options.attributes || {};\n\n if (typeof attributes.nonce === 'undefined') {\n var nonce = true ? __webpack_require__.nc : undefined;\n\n if (nonce) {\n attributes.nonce = nonce;\n }\n }\n\n Object.keys(attributes).forEach(function (key) {\n style.setAttribute(key, attributes[key]);\n });\n\n if (typeof options.insert === 'function') {\n options.insert(style);\n } else {\n var target = getTarget(options.insert || 'head');\n\n if (!target) {\n throw new Error(\"Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.\");\n }\n\n target.appendChild(style);\n }\n\n return style;\n}\n\nfunction removeStyleElement(style) {\n // istanbul ignore if\n if (style.parentNode === null) {\n return false;\n }\n\n style.parentNode.removeChild(style);\n}\n/* istanbul ignore next */\n\n\nvar replaceText = function replaceText() {\n var textStore = [];\n return function replace(index, replacement) {\n textStore[index] = replacement;\n return textStore.filter(Boolean).join('\\n');\n };\n}();\n\nfunction applyToSingletonTag(style, index, remove, obj) {\n var css = remove ? '' : obj.media ? \"@media \".concat(obj.media, \" {\").concat(obj.css, \"}\") : obj.css; // For old IE\n\n /* istanbul ignore if */\n\n if (style.styleSheet) {\n style.styleSheet.cssText = replaceText(index, css);\n } else {\n var cssNode = document.createTextNode(css);\n var childNodes = style.childNodes;\n\n if (childNodes[index]) {\n style.removeChild(childNodes[index]);\n }\n\n if (childNodes.length) {\n style.insertBefore(cssNode, childNodes[index]);\n } else {\n style.appendChild(cssNode);\n }\n }\n}\n\nfunction applyToTag(style, options, obj) {\n var css = obj.css;\n var media = obj.media;\n var sourceMap = obj.sourceMap;\n\n if (media) {\n style.setAttribute('media', media);\n } else {\n style.removeAttribute('media');\n }\n\n if (sourceMap && btoa) {\n css += \"\\n/*# sourceMappingURL=data:application/json;base64,\".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), \" */\");\n } // For old IE\n\n /* istanbul ignore if */\n\n\n if (style.styleSheet) {\n style.styleSheet.cssText = css;\n } else {\n while (style.firstChild) {\n style.removeChild(style.firstChild);\n }\n\n style.appendChild(document.createTextNode(css));\n }\n}\n\nvar singleton = null;\nvar singletonCounter = 0;\n\nfunction addStyle(obj, options) {\n var style;\n var update;\n var remove;\n\n if (options.singleton) {\n var styleIndex = singletonCounter++;\n style = singleton || (singleton = insertStyleElement(options));\n update = applyToSingletonTag.bind(null, style, styleIndex, false);\n remove = applyToSingletonTag.bind(null, style, styleIndex, true);\n } else {\n style = insertStyleElement(options);\n update = applyToTag.bind(null, style, options);\n\n remove = function remove() {\n removeStyleElement(style);\n };\n }\n\n update(obj);\n return function updateStyle(newObj) {\n if (newObj) {\n if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) {\n return;\n }\n\n update(obj = newObj);\n } else {\n remove();\n }\n };\n}\n\nmodule.exports = function (list, options) {\n options = options || {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of