diff --git a/public/js/main.js b/public/js/main.js index ff0eac39..6f996bde 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -1,72 +1,85 @@ -const deleteBtn = document.querySelectorAll('.fa-trash') -const item = document.querySelectorAll('.item span') -const itemCompleted = document.querySelectorAll('.item span.completed') +// The frontend client-side code that wires up the todo list UI to te backend routes -Array.from(deleteBtn).forEach((element)=>{ - element.addEventListener('click', deleteItem) -}) +// Select all delete icons, all todo text spans and all completed todos, to let these attach +// behaviour to each matching element in the NodeList +const deleteBtn = document.querySelectorAll(".fa-trash"); +const item = document.querySelectorAll(".item span"); +const itemCompleted = document.querySelectorAll(".item span.completed"); -Array.from(item).forEach((element)=>{ - element.addEventListener('click', markComplete) -}) +// For every delete icon, add a click listener that triggers the deleteItem function/handler. +Array.from(deleteBtn).forEach((element) => { + element.addEventListener("click", deleteItem); +}); -Array.from(itemCompleted).forEach((element)=>{ - element.addEventListener('click', markUnComplete) -}) +// For every incomplete todo text span, add a click listener that triggers the markComplete function/handler. +Array.from(item).forEach((element) => { + element.addEventListener("click", markComplete); +}); -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() +// For every completed todo text span, add a click listener that triggers the markUnComplete function/handler. +Array.from(itemCompleted).forEach((element) => { + element.addEventListener("click", markUnComplete); +}); - }catch(err){ - console.log(err) - } +// A handler for deleting a todo items, it runs in the context of the clicked delete icon (`this`). +// Reads the text of the todo from the DOM, sends it to the server in a DELETE request. +async function deleteItem() { + // Go from the clicked this icon to its parent element, then to the second child node which is the span containing the text, then grab the text of the todo item. + const itemText = this.parentNode.childNodes[1].innerText; + try { + const response = await fetch("deleteItem", { + method: "delete", + headers: { "Content-Type": "application/json" }, + // Send the todo text in the request body so the server knows which item to delete. + body: JSON.stringify({ + itemFromJS: itemText, + }), + }); + // Get the JSON response from the server and log it, then reload the page to reflect the change + const data = await response.json(); + console.log(data); + location.reload(); + } catch (err) { + // If anything goes wrong with the request, log the error. + 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() - - }catch(err){ - console.log(err) - } +// Handler for marking a todo as complete, similar to deleteItem, but sends a PUT/update request +// to the markComplete route, expecting the server to update the completed state of the item +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(); + } 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() - - }catch(err){ - console.log(err) - } -} \ No newline at end of file +// Handler for marking a todo as not complete, Sends a PUT/update request to the markUnComplete +// route so the server can toggle the state to completed: false +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(); + } catch (err) { + console.log(err); + } +} diff --git a/server.js b/server.js index 58b53e2f..7b4df452 100644 --- a/server.js +++ b/server.js @@ -1,93 +1,129 @@ -const express = require('express') -const app = express() -const MongoClient = require('mongodb').MongoClient -const PORT = 2121 -require('dotenv').config() +// Backend server-side code for the todo list app +// Import the Express framework +const express = require("express"); +// create an instance of the express application +const app = express(); +// Import the MongoDB client in order to connect to the database +const MongoClient = require("mongodb").MongoClient; + +// Set a default port to 2121 used when not running the app locally +const PORT = 2121; + +// Load environment variables from a .env file +require("dotenv").config(); + +// set variable names for the database connection string and the database name 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()) +// selecting, configuring and connecting to the specific database using dbConnectionStr +MongoClient.connect(dbConnectionStr, { useUnifiedTopology: true }).then((client) => { + console.log(`Connected to ${dbName} Database`); + db = client.db(dbName); +}); +// Configuring Express: +// - Set the view/template engine to ejs +// - server the static assets from the public folder +// - parse url encoded form data into the request body +// - parse json request bodies into the request body +app.set("view engine", "ejs"); +app.use(express.static("public")); +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)) -}) +// Handle get requests to the root url/front page of the app. Fetch all todo items and the count of +// incomplete items from MongoDB, then render the index.ejs template with that data. +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)); +}); -app.post('/addTodo', (request, response) => { - db.collection('todos').insertOne({thing: request.body.todoItem, completed: false}) - .then(result => { - console.log('Todo Added') - response.redirect('/') +// Handle post requests to the /addTodo url, when a new todo form is submitted. Insert the new item +// into the todo collection, then redirect to the homepage to reload the page including the change. +app.post("/addTodo", (request, response) => { + db.collection("todos") + .insertOne({ thing: request.body.todoItem, completed: false }) + .then((result) => { + console.log("Todo Added"); + response.redirect("/"); }) - .catch(error => console.error(error)) -}) + .catch((error) => console.error(error)); +}); -app.put('/markComplete', (request, response) => { - db.collection('todos').updateOne({thing: request.body.itemFromJS},{ +// Handle put requests to /markComplete. Find the document where the thing field match the text +// sent from the client and set its completed flag to true. If multiple match, update the most +// recent one. Do not create a new document if none match (upsert: false). +app.put("/markComplete", (request, response) => { + db.collection("todos") + .updateOne( + { thing: request.body.itemFromJS }, + { $set: { - completed: true - } - },{ - sort: {_id: -1}, - upsert: false + completed: true, + }, + }, + { + sort: { _id: -1 }, + upsert: false, + } + ) + .then((result) => { + console.log("Marked Complete"); + response.json("Marked Complete"); }) - .then(result => { - console.log('Marked Complete') - response.json('Marked Complete') - }) - .catch(error => console.error(error)) - -}) + .catch((error) => console.error(error)); +}); -app.put('/markUnComplete', (request, response) => { - db.collection('todos').updateOne({thing: request.body.itemFromJS},{ +// Same as markComplete, but set completed to false +app.put("/markUnComplete", (request, response) => { + db.collection("todos") + .updateOne( + { thing: request.body.itemFromJS }, + { $set: { - completed: false - } - },{ - sort: {_id: -1}, - upsert: false - }) - .then(result => { - console.log('Marked Complete') - response.json('Marked Complete') + completed: false, + }, + }, + { + sort: { _id: -1 }, + upsert: false, + } + ) + .then((result) => { + console.log("Marked Complete"); + response.json("Marked Complete"); }) - .catch(error => console.error(error)) + .catch((error) => console.error(error)); +}); -}) - -app.delete('/deleteItem', (request, response) => { - db.collection('todos').deleteOne({thing: request.body.itemFromJS}) - .then(result => { - console.log('Todo Deleted') - response.json('Todo Deleted') +// Handle delete requests to /deleteItem. Remove a single todo from the collection whose thing value matches the text sent from the client +app.delete("/deleteItem", (request, response) => { + db.collection("todos") + .deleteOne({ thing: request.body.itemFromJS }) + .then((result) => { + console.log("Todo Deleted"); + response.json("Todo Deleted"); }) - .catch(error => console.error(error)) - -}) + .catch((error) => console.error(error)); +}); -app.listen(process.env.PORT || PORT, ()=>{ - console.log(`Server running on port ${PORT}`) -}) \ No newline at end of file +// Start the server, using the hosting provider's port if available, otherwise use port 2121 +app.listen(process.env.PORT || PORT, () => { + console.log(`Server running on port ${PORT}`); +});