diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c3c81b8b..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "editor.fontSize": 42, - "terminal.integrated.fontSize": 62 -} \ No newline at end of file diff --git a/README.md b/README.md index 5ab0326c..6897d92b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ -npm install -add DB_STRING to .env file \ No newline at end of file +# ToDo List - EJS, MongoDB, Node + Express + +## Getting Started + +1. Go to the [todo-list-express](https://github.com/100devs/todo-list-express) repository and click `Fork` on the top right. This is basically "Save As"ing the project to your own GitHub. +1. In your forked repository, click `Code` -> `HTTPS` -> copy the link +1. Choose a location on your local PC where you want to create a copy of these files to work on. +1. Use the terminal to access that location, and type: `git clone [paste link copied during step 2]` + +Now you can work on the code. The assignment is to comment every line. Start with server.js, then move to index.ejs, main.js, and style.css if you want. + +## Running the App + +> Note: This technically isn't part of the assignment. + +### Orginal README said: run `npm install` + +- Run `npm install` from the terminal while in your repository's folder. + - This command goes through the `package.json` file and installs all dependencies listed there so the app can work. + +### Original README said: add DB_STRING to .env file + +- Create a file called `.env`, and type `DB_STRING=your-MongoDB-connection-string`. +- **Note**: If you need help, follow along with this [article](https://zellwk.com/blog/crud-express-mongodb/#setting-up-mongodb-atlas) to get your connection string. +- Add your connection string to your `.env` file. It should look something like: + - `DB_STRING=mongodb+srv://...` + - Continue with your _full connection string_. This is sensitive information, which is why it's being stored in your `.env`, and your `.gitignore` file lists `.env` so that it is not shared to your public github repository. + +1. Start the node server by running this command in a terminal within your repository folder: `node server.js` + +- **Note**: If running properly, the terminal will state: + - `Server running on port 2121` + - `Connected to todo Database` + +1. Open the application by going to `http://127.0.0.1:2121/` or `http://localhost:2121/` + +- The server is running on port 2121 on your local machine, which has the address of 127.0.0.1 (or localhost) when referring to itself. + +## Updates + +1. added styling +1. change text at top dynamically if all tasks get completed +1. added dark/light mode toggle + +### Todos + +- [] update app to use list item ids instead of DOM text to make changes +- [] improve list responsiveness on tiny screens diff --git a/public/css/normalize.css b/public/css/normalize.css new file mode 100644 index 00000000..c4699894 --- /dev/null +++ b/public/css/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/public/css/style.css b/public/css/style.css index 0475253a..5c4bf7b3 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1,7 +1,318 @@ -h1{ - color: red; +:root { + /* colors */ + --paper-background-color: #f7f7e6; + --completed-color: #bdbdbd; + --primary-dark-color: #0d1117; + --secondary-dark-color: #282a36; + --primary-light-color: #f8f8f8; + --secondary-light-color: #8f8f8f; + --accent-color: #73cfee; + --action-color: #dd6387; + + /* shadows */ + --accent-box-glow: 0 0 0.5em var(--accent-color); + --accent-text-outline: -2px -2px 0 var(--accent-color), + 2px -2px 0 var(--accent-color), -2px 2px 0 var(--accent-color), + 2px 2px 0 var(--accent-color); + --action-text-glow: 0.1em 0.1em 0.2em rgba(221, 99, 135, 0.5); + --light-text-glow: 0 0 0.2em var(--primary-light-color); + --accent-box-shadow: 0 -0.2em 0.2em rgba(115, 207, 238, 0.5); + --light-box-shadow: 0 -0.2em 0.2em rgba(143, 143, 143, 0.5); + + /* transitions */ + --transition: all 0.3s ease-in-out; +} + +/* overall document settings */ +body { + background-color: var(--primary-light-color); + color: var(--secondary-light-color); + font-family: system-ui, -apple-system, BlinkMacSystemFont, + 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', + 'Helvetica Neue', sans-serif; + line-height: 1.5; + + /* main grid */ + display: grid; + grid: auto 1fr auto / 10vw 1fr 10vw; + grid-template-areas: + 'header header header' + 'main main main' + 'footer footer footer'; + min-height: 100vh; +} + +a, +a *, +button, +button * { + cursor: pointer; +} + +/* header */ +header { + grid-area: header; + text-align: center; + font-size: clamp(1.75rem, 4vw + 1rem, 3rem); +} + +.title { + margin: 0; +} + +/* main content */ +main { + grid-area: main; +} + +.paperContainer { + position: relative; + width: 90%; + max-width: 800px; + min-width: 400px; + height: 480px; + margin: 0 auto; + background: var(--paper-background-color); + border: 1px solid var(--secondary-light-color); + border-radius: 10px; + overflow: auto; +} + +.paperContainer:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 60px; + background: radial-gradient( + var(--secondary-light-color) 8px, + transparent 2px + ) + repeat-y; + background-size: 30px 30px; + border-right: 3px solid var(--action-color); + box-sizing: border-box; +} + +.paperContent { + position: absolute; + top: 30px; + right: 0; + bottom: 30px; + left: 60px; + background: linear-gradient( + transparent, + transparent 28px, + var(--accent-color) 28px + ); + background-size: 100% 30px; + overflow-y: auto; +} + +.todosLeft { + margin: 0 0 0 0.25em; +} + +.todoItems { + margin-top: -3px; +} + +.paperContent .item { + display: flex; + align-items: center; + height: 30px; + line-height: 30px; + font-size: 1.3em; + font-weight: bold; + font-family: 'Handlee', cursive; + z-index: 1; +} + +.item .fa-trash { + padding-left: 10px; +} + +.item span:hover, +.fa-trash:hover { + color: var(--action-color); + transition: var(--transition); +} + +.item:hover { + cursor: pointer; +} + +.completed { + color: var(--completed-color); + text-decoration: line-through; +} + +.addTodoContainer { + max-width: max-content; + margin: 0 auto; +} + +.addTodo { + margin-bottom: 0; +} + +input[type='text'] { + background: var(--paper-background-color); + color: var(--primary-dark-color); + border: 1px solid var(--accent-color); + padding: 12px 16px; + font-size: 1rem; + border-radius: 8px; + outline: none; + transition: var(--transition); + width: 250px; + box-shadow: 0 4px 8px rgba(115, 207, 238, 0.3); +} + +input[type='text']::placeholder { + color: var(--secondary-light-color); + font-style: italic; +} + +input[type='text']:focus { + border-color: var(--action-color); + box-shadow: var(--action-text-glow); +} + +#addTaskBtn { + background: var(--action-color); + color: var(--primary-light-color); + border: 1px solid var(--action-color); + padding: 12px 24px; + font-size: 1rem; + font-weight: bold; + border-radius: 8px; + cursor: pointer; + transition: var(--transition); +} + +#addTaskBtn:hover { + box-shadow: var(--action-text-glow); + background: var(--primary-light-color); + color: var(--action-color); +} + +#addTaskBtn:active { + transform: scale(0.95); + box-shadow: inset 0 4px 8px rgba(221, 99, 135, 0.5); +} + +/* footer layout */ +footer { + grid-area: footer; + background-color: var(--secondary-light-color); + color: var(--primary-light-color); + box-shadow: var(--light-box-shadow); + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + padding: 10px; + margin-top: 15px; + font-size: 18px; + font-weight: 500; +} + +footer .fa-github, +footer .fa-linkedin { + color: var(--primary-light-color); + font-size: 24px; + transition: var(--transition); +} + +footer .fa-github:hover, +footer .fa-linkedin:hover { + transform: rotateY(360deg) scale(1.4); + color: var(--action-color); +} + +footer .fa-heart { + color: var(--action-color); + font-size: 24px; + transition: var(--transition); +} + +footer .fa-heart:hover { + transform: scale(1.3); +} + +/* Light mode */ +.light { + background-color: var(--primary-light-color); + color: var(--secondary-light-color); +} + +/* Dark theme overrides */ +body.dark { + background-color: var(--primary-dark-color); + color: var(--primary-light-color); +} + +.dark .paperContainer { + background-color: var(--secondary-dark-color); +} + +.dark .fa-trash { + color: var(--primary-light-color); +} + +.dark input[type='text'] { + background: var(--secondary-dark-color); + color: var(--primary-light-color); + border: 1px solid var(--secondary-light-color); + box-shadow: 0 4px 8px rgba(115, 207, 238, 0.3); +} + +.dark input[type='text']::placeholder { + color: var(--primary-light-color); +} + +.dark footer { + background-color: var(--secondary-dark-color); + box-shadow: var(--accent-box-shadow); +} + +/* Update button text based on theme */ +button.dark-mode-toggle { + position: absolute; + top: 1em; + right: 1em; + font-size: 1.5em; + background: none; + border: none; + border-radius: 50%; + padding: 0.3em; + cursor: pointer; + transition: var(--transition); +} + +button.dark-mode-toggle:hover { + box-shadow: var(--action-text-glow); +} + +button.dark-mode-toggle:active { + transform: scale(0.95); +} + +/* media queries */ +@media (max-width: 500px) { + #footer-span { + display: none; + } +} + +@media (max-width: 300px) { + header { + font-size: 1em; + } + + footer p { + display: none; + } } -.completed{ - color: gray; - text-decoration: line-through; -} \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..53134e8e Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/js/main.js b/public/js/main.js index ff0eac39..12afd567 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,72 +1,143 @@ -const deleteBtn = document.querySelectorAll('.fa-trash') -const item = document.querySelectorAll('.item span') -const itemCompleted = document.querySelectorAll('.item span.completed') +// select items from the DOM +const deleteBtns = document.querySelectorAll('.fa-trash'); +const items = document.querySelectorAll('.item span'); +const completedItems = document.querySelectorAll( + '.item span.completed' +); -Array.from(deleteBtn).forEach((element)=>{ - element.addEventListener('click', deleteItem) -}) +// querySelectorAll returns a NodeList since it is pulling things from the DOM +// a NodeList is an 'array-like' and can be turned into an array for manipulation +// next 3 lines creates arrays from the NodeLists selected at the top of the file and adds click event listeners to them, each running a different callback function +Array.from(deleteBtns).forEach((element) => { + element.addEventListener('click', deleteItem); +}); -Array.from(item).forEach((element)=>{ - element.addEventListener('click', markComplete) -}) +Array.from(items).forEach((element) => { + element.addEventListener('click', markComplete); +}); -Array.from(itemCompleted).forEach((element)=>{ - element.addEventListener('click', markUnComplete) -}) +Array.from(completedItems).forEach((element) => { + element.addEventListener('click', markUnComplete); +}); -async function deleteItem(){ - const itemText = this.parentNode.childNodes[1].innerText - try{ - const response = await fetch('deleteItem', { - method: 'delete', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - 'itemFromJS': itemText - }) - }) - const data = await response.json() - console.log(data) - location.reload() +// deletes an item from the todo list +// must be async because fetching data from the server +async function deleteItem() { + // select the text of the current item + const itemText = this.parentNode.childNodes[1].innerText; + // try to ask the server to delete an item with the `itemText` as the identifier + try { + // since async, must await the fetch from the server + // there must be a route on the server that matches 'deleteItem' + const response = await fetch('deleteItem', { + // 'delete' is the action to remove the item from the database + method: 'delete', + // lets the API know to expect JSON data + headers: { 'Content-Type': 'application/json' }, + // converts the javascript to JSON + body: JSON.stringify({ + itemFromJS: itemText, + }), + }); + // wait for API response and save into `data` variable + const data = await response.json(); + // print the response to the console + console.log(data); + // refresh the page to cause client to sent new GET req to show that the item has been deleted + location.reload(); - }catch(err){ - console.log(err) - } + // if something goes wrong, print the error to the console + } catch (err) { + console.log(err); + } } -async function markComplete(){ - const itemText = this.parentNode.childNodes[1].innerText - try{ - const response = await fetch('markComplete', { - method: 'put', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - 'itemFromJS': itemText - }) - }) - const data = await response.json() - console.log(data) - location.reload() +// marks a todo item as complete +// must be async because fetching data from the server +async function markComplete() { + // select the text of the current item + const itemText = this.parentNode.childNodes[1].innerText; + // try to ask the server to update an item with the `itemText` as the identifier + try { + // since async, must await the fetch from the server + // there must be a route on the server that matches 'markComplete' + const response = await fetch('markComplete', { + // 'put' is the action to update the item in the database + method: 'put', + // lets the API know to expect JSON data + headers: { 'Content-Type': 'application/json' }, + // converts the javascript to JSON + body: JSON.stringify({ + itemFromJS: itemText, + }), + }); + // wait for API response and save into `data` variable + const data = await response.json(); + // print the response to the console + console.log(data); + // refresh the page to cause client to sent new GET req to show that the item has been marked as completed + location.reload(); - }catch(err){ - console.log(err) - } + // if something goes wrong, print the error to the console + } catch (err) { + console.log(err); + } } -async function markUnComplete(){ - const itemText = this.parentNode.childNodes[1].innerText - try{ - const response = await fetch('markUnComplete', { - method: 'put', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ - 'itemFromJS': itemText - }) - }) - const data = await response.json() - console.log(data) - location.reload() +// marks a todo item as uncompleted +// must be async because fetching data from the server +async function markUnComplete() { + // select the text of the current item + const itemText = this.parentNode.childNodes[1].innerText; + // since async, must await the fetch from the server + // there must be a route on the server that matches 'markUnComplete' + try { + const response = await fetch('markUnComplete', { + // 'put' is the action to update the item in the database + method: 'put', + // lets the API know to expect JSON data + headers: { 'Content-Type': 'application/json' }, + // converts the javascript to JSON + body: JSON.stringify({ + itemFromJS: itemText, + }), + }); + // wait for API response and save into `data` variable + const data = await response.json(); + // print the response to the console + console.log(data); + // refresh the page to cause client to sent new GET req to show that the item has been marked as uncompleted + location.reload(); - }catch(err){ - console.log(err) - } -} \ No newline at end of file + // if something goes wrong, print the error to the console + } catch (err) { + console.log(err); + } +} + +// Check for saved theme preference in localStorage +const savedTheme = localStorage.getItem('theme'); +if (savedTheme) { + document.body.classList.add(savedTheme); +} + +// Toggle dark mode +const darkModeToggle = document.getElementById('darkModeToggle'); +darkModeToggle.addEventListener('click', () => { + const currentTheme = document.body.classList.contains('dark') + ? 'dark' + : 'light'; + + // Toggle theme + if (currentTheme === 'dark') { + document.body.classList.remove('dark'); + document.body.classList.add('light'); + darkModeToggle.textContent = '🌙'; // Change button text for light mode + localStorage.setItem('theme', 'light'); + } else { + document.body.classList.remove('light'); + document.body.classList.add('dark'); + darkModeToggle.textContent = '🌞'; // Change button text for dark mode + localStorage.setItem('theme', 'dark'); + } +}); diff --git a/server.js b/server.js index 58b53e2f..62607841 100644 --- a/server.js +++ b/server.js @@ -1,93 +1,142 @@ -const express = require('express') -const app = express() -const MongoClient = require('mongodb').MongoClient -const PORT = 2121 -require('dotenv').config() - - +// import express +const express = require('express'); +// import mongodb +const MongoClient = require('mongodb').MongoClient; +// set up dotenv +require('dotenv').config(); +// use express +const app = express(); +// set default port +const PORT = 2121; +// initialize db variables let db, - dbConnectionStr = process.env.DB_STRING, - dbName = 'todo' + dbConnectionStr = process.env.DB_STRING, + dbName = 'todo'; -MongoClient.connect(dbConnectionStr, { useUnifiedTopology: true }) - .then(client => { - console.log(`Connected to ${dbName} Database`) - db = client.db(dbName) - }) - -app.set('view engine', 'ejs') -app.use(express.static('public')) -app.use(express.urlencoded({ extended: true })) -app.use(express.json()) +// connect to MongoDB database +MongoClient.connect(dbConnectionStr, { + useUnifiedTopology: true, +}).then((client) => { + console.log(`Connected to ${dbName} Database`); + db = client.db(dbName); +}); +// let express know to expect ejs as view engine +app.set('view engine', 'ejs'); +// tell express to use static files inside the public folder +app.use(express.static('public')); +// replace bodyparser middleware, this is how express can parse JSON +app.use(express.urlencoded({ extended: true })); +app.use(express.json()); -app.get('/',async (request, response)=>{ - const todoItems = await db.collection('todos').find().toArray() - const itemsLeft = await db.collection('todos').countDocuments({completed: false}) - response.render('index.ejs', { items: todoItems, left: itemsLeft }) - // db.collection('todos').find().toArray() - // .then(data => { - // db.collection('todos').countDocuments({completed: false}) - // .then(itemsLeft => { - // response.render('index.ejs', { items: data, left: itemsLeft }) - // }) - // }) - // .catch(error => console.error(error)) -}) +// set up route for home page -> GET/Read +// is async because need to await req to mongodb for todo items +app.get('/', async (request, response) => { + // search db for all todo items, put into an array + const todoItems = await db.collection('todos').find().toArray(); + // count how many todos are not done + const itemsLeft = await db + .collection('todos') + .countDocuments({ completed: false }); + // render the DOM using the index.ejs template, supplying the todoItems as items and itemsLeft as left + response.render('index.ejs', { items: todoItems, left: itemsLeft }); +}); +// route to add a new todo item -> POST/Create app.post('/addTodo', (request, response) => { - db.collection('todos').insertOne({thing: request.body.todoItem, completed: false}) - .then(result => { - console.log('Todo Added') - response.redirect('/') + // access the todos db collection + db.collection('todos') + // insert one item (as thing) using the form to set the body, hard coding whether it is completed as false + .insertOne({ thing: request.body.todoItem, completed: false }) + // after the item is inserted (async) + .then((result) => { + // print to the console that the todo was added + console.log('Todo Added'); + // tell the client to redirect back to the home page (essentially a refresh) to cause a new GET request + response.redirect('/'); }) - .catch(error => console.error(error)) -}) + // if something goes wrong, print the error to the console + .catch((error) => console.error(error)); +}); +// route to mark a todo as complete -> PUT/Update app.put('/markComplete', (request, response) => { - db.collection('todos').updateOne({thing: request.body.itemFromJS},{ + // access the todos db collection + db.collection('todos') + // update the first todo item found based on the todo text grabbed from the DOM on the client-side + .updateOne( + { thing: request.body.itemFromJS }, + { + // set its completed attribute to true $set: { - completed: true - } - },{ - sort: {_id: -1}, - upsert: false - }) - .then(result => { - console.log('Marked Complete') - response.json('Marked Complete') + completed: true, + }, + }, + { + // sort the items in descending order + sort: { _id: -1 }, + // if no item found, do not add a new one + upsert: false, + } + ) + .then((result) => { + // print to the console that the item is marked complete + console.log('Marked Complete'); + // respond to client that item marked complete + response.json('Marked Complete'); }) - .catch(error => console.error(error)) - -}) + // if something goes wrong, print error to console + .catch((error) => console.error(error)); +}); +// route to mark a todo as uncompleted -> PUT/Update app.put('/markUnComplete', (request, response) => { - db.collection('todos').updateOne({thing: request.body.itemFromJS},{ + // access the todos db collection + db.collection('todos') + // update the first todo item found based on the todo text grabbed from the DOM on the client-side + .updateOne( + { thing: request.body.itemFromJS }, + { + // set its completed attribute to false $set: { - completed: false - } - },{ - sort: {_id: -1}, - upsert: false - }) - .then(result => { - console.log('Marked Complete') - response.json('Marked Complete') + completed: false, + }, + }, + { + // sort the items in descending order + sort: { _id: -1 }, + // if no item found, do not add a new one + upsert: false, + } + ) + .then((result) => { + // print to the console that the item is marked uncompleted + console.log('Marked Uncompleted'); + // respond to client that item marked uncompleted + response.json('Marked Uncompleted'); }) - .catch(error => console.error(error)) - -}) + // if something goes wrong, print error to console + .catch((error) => console.error(error)); +}); +// route to remove a todo from the list -> DELETE/Dee-ley-tey app.delete('/deleteItem', (request, response) => { - db.collection('todos').deleteOne({thing: request.body.itemFromJS}) - .then(result => { - console.log('Todo Deleted') - response.json('Todo Deleted') + // access the todos db collection + db.collection('todos') + // remove the first todo item found based on the todo text grabbed from the DOM on the client-side + .deleteOne({ thing: request.body.itemFromJS }) + .then((result) => { + // print to the console that the item was deleted + console.log('Todo Deleted'); + // respond to client that item was deleted + response.json('Todo Deleted'); }) - .catch(error => console.error(error)) - -}) + // if something goes wrong, print error to console + .catch((error) => console.error(error)); +}); -app.listen(process.env.PORT || PORT, ()=>{ - console.log(`Server running on port ${PORT}`) -}) \ No newline at end of file +// tell the server where to listen +app.listen(process.env.PORT || PORT, () => { + // print listening statement to console + console.log(`Server running on port ${PORT}`); +}); diff --git a/views/index.ejs b/views/index.ejs index a26617ae..b11df0bc 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -1,47 +1,107 @@ + + - - - - - Document - - - - - - - - - - - Document - - -

Todo List:

- + + + + + + + + + + ToDo List -

Left to do: <%= left %>

+ + + + -

Add A Todo:

+ + + + + +
+

Todo List

+
+
+ +
+
+

+ <% if ( left < 1 ) { %> + All Done! + <% } else { %> + <%= left %> Todos Left + <% } %> +

-
- - -
- +
    + + <% for(let i=0; i < items.length; i++) {%> +
  • + <% if(items[i].completed === true) {%> + <%= items[i].thing %> + <% }else{ %> + <%= items[i].thing %> + <% } %> + +
  • + <% } %> +
+
+
- - +
+

Add Todo:

+ +
+ + + +
+
+
+ +