├── .DS_Store ├── .babelrc ├── .eslintrc.json ├── .github └── workflows │ └── linters.yml ├── .gitignore ├── .hintrc ├── .stylelintrc.json ├── MIT.md ├── README.md ├── app_screenshot.png ├── dist ├── index.html └── main.js ├── index.html ├── main.js ├── one.png ├── package-lock.json ├── package.json ├── src ├── index.html ├── index.js ├── modules │ ├── todoList.js │ └── utils.js └── style.css ├── todoList.test.js ├── todolist.png └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iLynette/todo-list/342f85a2fb25421aeb23856177bf0d2e9cbccdfb/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": ["@babel/plugin-transform-modules-commonjs"] 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 2018, 10 | "sourceType": "module" 11 | }, 12 | "extends": ["airbnb-base"], 13 | "rules": { 14 | "no-shadow": "off", 15 | "no-use-before-define": "off", 16 | "no-unused-vars": "off", 17 | "no-param-reassign": "off", 18 | "eol-last": "off", 19 | "import/extensions": [ 1, { 20 | "js": "always", "json": "always" 21 | }] 22 | }, 23 | "ignorePatterns": [ 24 | "dist/", 25 | "build/" 26 | ] 27 | } -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | lighthouse: 10 | name: Lighthouse 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 Lighthouse 18 | run: npm install -g @lhci/cli@0.7.x 19 | - name: Lighthouse Report 20 | run: lhci autorun --upload.target=temporary-public-storage --collect.staticDistDir=. 21 | webhint: 22 | name: Webhint 23 | runs-on: ubuntu-18.04 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: "12.x" 29 | - name: Setup Webhint 30 | run: | 31 | npm install --save-dev hint@6.x 32 | [ -f .hintrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/html-css-js/.hintrc 33 | - name: Webhint Report 34 | run: npx hint . 35 | stylelint: 36 | name: Stylelint 37 | runs-on: ubuntu-18.04 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: "12.x" 43 | - name: Setup Stylelint 44 | run: | 45 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 46 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/html-css-js/.stylelintrc.json 47 | - name: Stylelint Report 48 | run: npx stylelint "**/*.{css,scss}" 49 | eslint: 50 | name: ESLint 51 | runs-on: ubuntu-18.04 52 | steps: 53 | - uses: actions/checkout@v2 54 | - uses: actions/setup-node@v1 55 | with: 56 | node-version: "12.x" 57 | - name: Setup ESLint 58 | run: | 59 | npm install --save-dev eslint@7.x eslint-config-airbnb-base@14.x eslint-plugin-import@2.x babel-eslint@10.x 60 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/html-css-js/.eslintrc.json 61 | - name: ESLint Report 62 | run: npx eslint . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "connector": { 3 | "name": "local", 4 | "options": { 5 | "pattern": ["**", "!.git/**", "!node_modules/**"] 6 | } 7 | }, 8 | "extends": ["development"], 9 | "formatters": ["stylish"], 10 | "hints": [ 11 | "button-type", 12 | "disown-opener", 13 | "html-checker", 14 | "meta-charset-utf-8", 15 | "meta-viewport", 16 | "no-inline-styles:error" 17 | ] 18 | } -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": null, 6 | "scss/at-rule-no-unknown": true, 7 | "csstree/validator": true 8 | }, 9 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 10 | } -------------------------------------------------------------------------------- /MIT.md: -------------------------------------------------------------------------------- 1 | ## Copyright 2021, [YOUR NAME] 2 | 3 | ###### Please delete this line and the next one 4 | ###### APP TYPE can be a webpage/website, a web app, a software and so on 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this [APP TYPE] and associated documentation files, to deal in the [APP TYPE] without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the [APP TYPE], and to permit persons to whom the [APP TYPE] is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the [APP TYPE]. 9 | 10 | THE [APP TYPE] 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 [APP TYPE] OR THE USE OR OTHER DEALINGS IN THE [APP TYPE]. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/badge/Microverse-blueviolet) 2 | # to do List 3 | > To do list is a simple CRUD application that allows you to add, delete, update tasks that you are supposed to do. 4 | 5 | ![screenshot](./one.png) 6 | ## Built With 7 | - HTML-5 8 | - CSS3 9 | - Javacript 10 | - Webpack 11 | - Linters 12 | 13 | ## Live Demo 14 | For the link to the live demo, [Click here](https://ilynette.github.io/todo-list/) 15 | 16 | ## Getting Started 17 | - To get a local copy up and running follow these simple example steps. 18 | 19 | ### Prerequisites 20 | 21 | A Web Browser (preferably Google Chrome) 22 | 23 | ### Setup 24 | - Open your command prompt or terminal and run. 25 | - Clone the GitHub Repository from 26 | - run this commands in your terminal: 27 | - `npm install` 28 | - `npm run build` 29 | - `npm start` 30 | 31 | ### Usage 32 | after running `npm start` the website will be opened automatically on your default browser. 33 | 34 | ### Run tests 35 | 36 | - To run tests run : 37 | - npx hint . 38 | - npx stylelint "**/*.{css,scss}" 39 | 40 | ### Deployment 41 | 42 | - Project is deployed using 43 | - git push 44 | ## Authors 45 | 46 | 👤 **Author** 47 | - GitHub: [@iLynette](https://github.com/iLynette) 48 | - Twitter: [@acholah_lynette](https://twitter.com/acholah_lynette) 49 | - LinkedIn: [acholah lynette](https://www.linkedin.com/in/lynette-acholah/) 50 | 51 | ## 🤝 Contributing 52 | 53 | Contributions, issues, and feature requests are welcome! 54 | 55 | Feel free to check the [issues page](../../issues/). 56 | ## :memo: License 57 | This project is [MIT](./MIT.md) licensed. 58 | -------------------------------------------------------------------------------- /app_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iLynette/todo-list/342f85a2fb25421aeb23856177bf0d2e9cbccdfb/app_screenshot.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | To Do list 9 | 10 | 11 |
12 |
13 |

Today's To Do

14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | To Do list 9 | 10 | 11 |
12 |
13 |

Today's To Do

14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iLynette/todo-list/342f85a2fb25421aeb23856177bf0d2e9cbccdfb/one.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list", 3 | "version": "1.0.0", 4 | "description": "![](https://img.shields.io/badge/Microverse-blueviolet)", 5 | "private": "true", 6 | "scripts": { 7 | "test": "jest --env=jsdom", 8 | "build": "webpack", 9 | "start": "webpack serve --open" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/iLynette/todo-list.git" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/iLynette/todo-list/issues" 20 | }, 21 | "homepage": "https://github.com/iLynette/todo-list#readme", 22 | "devDependencies": { 23 | "@babel/plugin-transform-modules-commonjs": "^7.16.8", 24 | "babel-eslint": "^10.1.0", 25 | "css-loader": "^6.6.0", 26 | "eslint": "^7.32.0", 27 | "eslint-config-airbnb-base": "^14.2.1", 28 | "eslint-plugin-import": "^2.25.4", 29 | "hint": "^6.1.9", 30 | "html-webpack-plugin": "^5.5.0", 31 | "jest": "^27.5.1", 32 | "style-loader": "^3.3.1", 33 | "stylelint": "^13.13.1", 34 | "stylelint-config-standard": "^21.0.0", 35 | "stylelint-csstree-validator": "^1.9.0", 36 | "stylelint-scss": "^3.21.0", 37 | "webpack": "^5.69.0", 38 | "webpack-cli": "^4.9.2", 39 | "webpack-dev-server": "^4.7.4" 40 | }, 41 | "dependencies": { 42 | "lodash": "^4.17.21" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | To Do list 9 | 10 | 11 |
12 |
13 |

Today's To Do

14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; //eslint-disable-line 2 | import './style.css'; 3 | import * as gin from './modules/utils.js'; 4 | 5 | gin.genesis(); 6 | -------------------------------------------------------------------------------- /src/modules/todoList.js: -------------------------------------------------------------------------------- 1 | export default class TodoList { 2 | todos = []; 3 | 4 | addTodo = (description) => { 5 | const todo = { 6 | description, 7 | index: this.todos.length + 1, 8 | completed: false, 9 | id: Date.now(), 10 | }; 11 | this.todos.push(todo); 12 | this.save(); 13 | }; 14 | 15 | save = () => { 16 | localStorage.setItem('todos', JSON.stringify(this.todos)); 17 | }; 18 | 19 | delete = (id) => { 20 | this.todos = this.todos.filter((t) => t.id !== id); 21 | this.sort(); 22 | this.save(); 23 | return id; 24 | }; 25 | 26 | sort= () => { 27 | this.todos.sort((a, b) => a.index - b.index); 28 | for (let i = 0; i < this.todos.length; i += 1) { 29 | this.todos[i].index = i + 1; 30 | } 31 | }; 32 | 33 | edit = (todo = {}) => { 34 | this.todos[todo.index - 1] = todo; 35 | this.save(); 36 | return todo; 37 | }; 38 | 39 | getTodos = (i = 1) => { 40 | this.todos = JSON.parse(localStorage.getItem('todos')); 41 | return this.todos[i - 1]; 42 | }; 43 | 44 | clearComplete = () => { 45 | this.todos = this.todos.filter((t) => !t.completed); 46 | this.sort(); 47 | this.save(); 48 | }; 49 | } -------------------------------------------------------------------------------- /src/modules/utils.js: -------------------------------------------------------------------------------- 1 | import TodoList from './todoList.js'; 2 | 3 | let todo = new TodoList(); 4 | const todoList = document.getElementById('dynamic-list'); 5 | const form = document.getElementById('form'); 6 | export const allTasks = (T) => { 7 | if (T !== null && T !== undefined) { 8 | todo = T; 9 | } 10 | document.getElementById('dynamic-list').innerHTML = ''; 11 | todo.todos 12 | .sort((a, b) => a.index - b.index) 13 | .map((todos) => { 14 | const todoCard = document.createElement('form'); 15 | todoCard.classList.add('one'); 16 | todoCard.setAttribute('id', todos.id); 17 | const checkBox = document.createElement('input'); 18 | checkBox.setAttribute('type', 'checkbox'); 19 | if (todos.completed) { 20 | checkBox.setAttribute('checked', 'checked'); 21 | } 22 | checkBox.addEventListener('change', () => onchange(todos)); 23 | todoCard.appendChild(checkBox); 24 | const description = document.createElement('p'); 25 | if (todos.completed) { 26 | description.style.textDecoration = 'line-through'; 27 | } 28 | description.innerHTML = todos.description; 29 | todoCard.appendChild(description); 30 | const button = document.createElement('button'); 31 | button.onclick = () => editDescription(todos); 32 | const icon = document.createElement('i'); 33 | icon.classList.add('fas', 'fa-ellipsis-v'); 34 | button.appendChild(icon); 35 | todoCard.appendChild(button); 36 | document.getElementById('dynamic-list').appendChild(todoCard); 37 | 38 | return todoCard; 39 | }); 40 | }; 41 | const editDescription = (todos = {}) => { 42 | const todoCard = document.getElementById(todos.id); 43 | todoCard.replaceChildren(); 44 | const checkBox = document.createElement('input'); 45 | checkBox.setAttribute('type', 'checkbox'); 46 | if (todos.completed) { 47 | checkBox.setAttribute('checked', 'checked'); 48 | } 49 | checkBox.addEventListener('change', () => onchange(todos)); 50 | todoCard.appendChild(checkBox); 51 | const editField = document.createElement('input'); 52 | editField.style.padding = '20px'; 53 | editField.style.width = '100%'; 54 | editField.style.border = 'none'; 55 | editField.value = todos.description; 56 | todoCard.addEventListener('keydown', (e) => { 57 | if (e.key === 'Enter') { 58 | const edit = editField.value; 59 | if (edit.trim().length > 0) { 60 | todos.description = edit; 61 | todo.edit(todos); 62 | allTasks(); 63 | } 64 | } 65 | }); 66 | todoCard.appendChild(editField); 67 | const button = document.createElement('button'); 68 | button.onclick = () => { 69 | todo.delete(todos.id); 70 | allTasks(); 71 | }; 72 | const icon = document.createElement('i'); 73 | icon.classList.add('fas', 'fa-trash'); 74 | button.appendChild(icon); 75 | todoCard.appendChild(button); 76 | }; 77 | const onchange = (todos = {}) => { 78 | todos.completed = !todos.completed; 79 | todo.edit(todos); 80 | allTasks(); 81 | }; 82 | const checkDb = () => { 83 | if (localStorage.getItem('todos')) { 84 | todo.getTodos(); 85 | allTasks(); 86 | } else { 87 | // show use no todos 88 | } 89 | }; 90 | 91 | const addNewToDo = () => { 92 | const todoTask = document.getElementById('list-activity').value; 93 | if (todoTask.trim().length > 0) { 94 | todo.addTodo(todoTask); 95 | form.reset(); 96 | allTasks(); 97 | } 98 | }; 99 | export const genesis = () => { 100 | checkDb(); 101 | form.addEventListener('keydown', (e) => { 102 | if (e.key === 'Enter') { 103 | addNewToDo(); 104 | } 105 | }); 106 | document.getElementById('remove').onclick = () => { 107 | todo.clearComplete(); 108 | allTasks(); 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(241, 239, 239); 3 | width: 100%; 4 | height: 80vh; 5 | } 6 | 7 | #todo-container { 8 | background-color: #fff; 9 | width: 80%; 10 | box-shadow: 0 4px 0 rgb(209, 207, 207); 11 | margin: 0 auto; 12 | } 13 | 14 | #title { 15 | margin-top: 20%; 16 | padding: 0 20px; 17 | display: flex; 18 | justify-content: space-between; 19 | align-items: center; 20 | border-bottom: 1px solid rgb(211, 209, 209); 21 | color: rgb(85, 81, 81); 22 | font-size: 15px; 23 | } 24 | 25 | #title h3 { 26 | padding-left: 20px; 27 | } 28 | 29 | .list-container { 30 | display: flex; 31 | padding: 0 20px; 32 | justify-content: space-between; 33 | align-items: center; 34 | border-bottom: 1px solid rgb(211, 209, 209); 35 | } 36 | 37 | .list-container input { 38 | border: 1px hidden; 39 | padding: 20px; 40 | font-size: 15px; 41 | } 42 | 43 | .one { 44 | display: flex; 45 | align-items: center; 46 | padding: 5px; 47 | border-bottom: 1px solid rgb(211, 209, 209); 48 | padding-left: 25px; 49 | } 50 | 51 | .one p { 52 | padding-left: 10px; 53 | color: rgb(105, 102, 102); 54 | } 55 | 56 | .one button { 57 | position: absolute; 58 | right: 12%; 59 | cursor: pointer; 60 | border: none; 61 | background-color: transparent; 62 | } 63 | 64 | .delete { 65 | background-color: rgb(241, 239, 239); 66 | justify-content: center; 67 | } 68 | 69 | #remove { 70 | border: 1px hidden; 71 | text-align: center; 72 | width: 100%; 73 | background-color: #fff; 74 | padding: 15px; 75 | color: rgb(104, 102, 102); 76 | font-size: 16px; 77 | } 78 | -------------------------------------------------------------------------------- /todoList.test.js: -------------------------------------------------------------------------------- 1 | import TodoList from './src/modules/todoList.js'; 2 | import * as gin from './src/modules/utils.js'; 3 | 4 | class LocalStorageMock { 5 | constructor() { 6 | this.store = {}; 7 | } 8 | 9 | clear() { 10 | this.store = {}; 11 | } 12 | 13 | getItem(key) { 14 | return this.store[key] || null; 15 | } 16 | 17 | setItem(key, value) { 18 | this.store[key] = String(value); 19 | } 20 | 21 | removeItem(key) { 22 | delete this.store[key]; 23 | } 24 | } 25 | 26 | global.localStorage = new LocalStorageMock(); 27 | 28 | document.body.innerHTML = `
+ 29 |
30 |
`; 31 | const todoList = new TodoList(); 32 | 33 | describe('Add task', () => { 34 | test('Add one task expect tasks length to be 1', () => { 35 | todoList.addTodo('go shopping'); 36 | todoList.addTodo('watch a movie'); 37 | expect(todoList.todos.length).toBe(2); 38 | }); 39 | }); 40 | 41 | describe('Add the added todo to the DOM', () => { 42 | test('One task has been added expect one task element in DOM', () => { 43 | gin.allTasks(todoList); 44 | const tasksList = document.getElementById('dynamic-list'); 45 | expect(tasksList.childNodes.length).toBe(2); 46 | }); 47 | }); 48 | 49 | describe('Remove first todo from todoList', () => { 50 | test('One task has been remove expect zero', () => { 51 | const { id } = todoList.todos[0]; 52 | const taskCard = document.getElementById(id); 53 | taskCard.parentNode.removeChild(taskCard); 54 | expect(todoList.delete(id)).toBe(id); 55 | }); 56 | }); 57 | 58 | describe('Check if task card had been removed from DOM', () => { 59 | test('One card removed expect node children to be 1', () => { 60 | const tasksList = document.getElementById('dynamic-list'); 61 | expect(tasksList.childNodes.length).toBe(1); 62 | }); 63 | }); 64 | 65 | describe('edit todos', () => { 66 | test('Edited description should be equal to new value', () => { 67 | todoList.addTodo('watch a movie'); 68 | const todo = todoList.todos[0]; 69 | todo.description = 'finish the week'; 70 | expect(todoList.edit(todo).description).toBe(todo.description); 71 | }); 72 | }); 73 | 74 | describe('update completed to true', () => { 75 | test('Todo marked as completed , should be truthy', () => { 76 | const todo = todoList.todos[0]; 77 | todo.completed = true; 78 | expect(todoList.edit(todo).completed).toBeTruthy(); 79 | }); 80 | }); 81 | 82 | describe('clear all completed', () => { 83 | test('One task marked as completed and cleared, todos should be 0', () => { 84 | todoList.clearComplete(); 85 | expect(todoList.todos.length).toBe(0); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /todolist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iLynette/todo-list/342f85a2fb25421aeb23856177bf0d2e9cbccdfb/todolist.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/index.js', 7 | devServer: { 8 | static: './dist', 9 | }, 10 | plugins: [ 11 | new HtmlWebpackPlugin({ 12 | template: './src/index.html', 13 | }), 14 | ], 15 | output: { 16 | filename: 'main.js', 17 | path: path.resolve(__dirname, 'dist'), 18 | clean: true, 19 | publicPath: '/', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.css$/i, 25 | use: ['style-loader', 'css-loader'], 26 | }, 27 | ], 28 | }, 29 | }; --------------------------------------------------------------------------------