commit 4657a24c34e743f742ddf8f6deb4a00aeb5d2b59 Author: julien Lengrand-Lambert Date: Fri May 28 00:37:58 2021 +0200 Initial commit diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..26786f9 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9306c8a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to Node Functions", + "type": "node", + "request": "attach", + "port": 9229, + "preLaunchTask": "func: host start" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc8c902 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "azureFunctions.deploySubpath": "api", + "azureFunctions.postDeployTask": "npm install", + "azureFunctions.projectLanguage": "JavaScript", + "azureFunctions.projectRuntime": "~3", + "debug.internalConsoleOptions": "neverOpen", + "azureFunctions.preDeployTask": "npm prune" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ad2191b --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,32 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "command": "host start", + "problemMatcher": "$func-node-watch", + "isBackground": true, + "dependsOn": "npm install", + "options": { + "cwd": "${workspaceFolder}/api" + } + }, + { + "type": "shell", + "label": "npm install", + "command": "npm install", + "options": { + "cwd": "${workspaceFolder}/api" + } + }, + { + "type": "shell", + "label": "npm prune", + "command": "npm prune --production", + "problemMatcher": [], + "options": { + "cwd": "${workspaceFolder}/api" + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..187aa96 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Static Web Apps - Mongoose starter + +This project is designed to be a starter [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps/overview) with [Mongoose](https://mongoosejs.com/). It uses the following resources: + +- Azure resources + - Azure Static Web Apps + - Cosmos DB with Mongo API +- Application libraries + - Vanilla JavaScript + - Mongoose + - Azure Functions diff --git a/api/.funcignore b/api/.funcignore new file mode 100644 index 0000000..5179222 --- /dev/null +++ b/api/.funcignore @@ -0,0 +1,7 @@ +*.js.map +*.ts +.git* +.vscode +local.settings.json +test +tsconfig.json \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore new file mode 100644 index 0000000..772851c --- /dev/null +++ b/api/.gitignore @@ -0,0 +1,94 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TypeScript output +dist +out + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json \ No newline at end of file diff --git a/api/host.json b/api/host.json new file mode 100644 index 0000000..6ab6643 --- /dev/null +++ b/api/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[1.*, 2.0.0)" + } +} diff --git a/api/package-lock.json b/api/package-lock.json new file mode 100644 index 0000000..507bac9 --- /dev/null +++ b/api/package-lock.json @@ -0,0 +1,241 @@ +{ + "name": "api", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", + "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "bson": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", + "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "kareem": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.1.tgz", + "integrity": "sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw==" + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "mongodb": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.2.tgz", + "integrity": "sha512-sSZOb04w3HcnrrXC82NEh/YGCmBuRgR+C1hZgmmv4L6dBz4BkRse6Y8/q/neXer9i95fKUBbFi4KgeceXmbsOA==", + "requires": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "mongoose": { + "version": "5.10.11", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.10.11.tgz", + "integrity": "sha512-R5BFitKW94/S/Z48w+X+qi/eto66jWBcVEVA8nYVkBoBAPFGq7JSYP/0uso+ZHs+7XjSzTuui+SUllzxIrf9yA==", + "requires": { + "bson": "^1.1.4", + "kareem": "2.3.1", + "mongodb": "3.6.2", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.7.0", + "mquery": "3.2.2", + "ms": "2.1.2", + "regexp-clone": "1.0.0", + "safe-buffer": "5.2.1", + "sift": "7.0.1", + "sliced": "1.0.1" + } + }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, + "mpath": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.7.0.tgz", + "integrity": "sha512-Aiq04hILxhz1L+f7sjGyn7IxYzWm1zLNNXcfhDtx04kZ2Gk7uvFdgZ8ts1cWa/6d0TQmag2yR8zSGZUmp0tFNg==" + }, + "mquery": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.2.tgz", + "integrity": "sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q==", + "requires": { + "bluebird": "3.5.1", + "debug": "3.1.0", + "regexp-clone": "^1.0.0", + "safe-buffer": "5.1.2", + "sliced": "1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "regexp-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", + "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "sift": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-7.0.1.tgz", + "integrity": "sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g==" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + } + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..1386617 --- /dev/null +++ b/api/package.json @@ -0,0 +1,13 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "scripts": { + "start": "func start", + "test": "echo \"No tests yet...\"" + }, + "dependencies": { + "mongoose": "^5.10.11" + }, + "devDependencies": {} +} diff --git a/api/proxies.json b/api/proxies.json new file mode 100644 index 0000000..b385252 --- /dev/null +++ b/api/proxies.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/proxies", + "proxies": {} +} diff --git a/api/tasks/function.json b/api/tasks/function.json new file mode 100644 index 0000000..eaeddc4 --- /dev/null +++ b/api/tasks/function.json @@ -0,0 +1,21 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post", + "put" + ], + "route": "tasks/{id?}" + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ] +} diff --git a/api/tasks/index.js b/api/tasks/index.js new file mode 100644 index 0000000..a04ebed --- /dev/null +++ b/api/tasks/index.js @@ -0,0 +1,88 @@ +// Load mongoose +const mongoose = require('mongoose'); + +// Connect to the database +mongoose.connect( + process.env.CONNECTION_STRING, // Retrieve connection string + { // boiler plate values + useNewUrlParser: true, + useUnifiedTopology: true, + } +); + +// Create the schema or structure of our object in Mongoose +const taskSchema = new mongoose.Schema({ + title: String, // Add title property of type string + completed: { // Add completed property + type: Boolean, // Set type to boolean + default: false // Set default to false + } +}); + +// Create a model using our schema +// This model will be used to access the database +const TaskModel = mongoose.model('task', taskSchema); + +// Export our function +module.exports = async function (context, req) { + // setup our default content type (we always return JSON) + context.res = { + header: { + "Content-Type": "application/json" + } + } + + // Read the method and determine the requested action + switch (req.method) { + // If get, return all tasks + case 'GET': + await getTasks(context); + break; + // If post, create new task + case 'POST': + await createTask(context); + break; + // If put, update task + case 'PUT': + await updateTask(context); + break; + } +}; + +// Return all tasks +async function getTasks(context) { + // load all tasks from database + const tasks = await TaskModel.find(); + // return all tasks + context.res.body = { tasks: tasks }; +} + +// Create new task +async function createTask(context) { + // Read the uploaded task + const body = context.req.body; + // Save to database + const task = await TaskModel.create(body); + // Set the HTTP status to created + context.res.status = 201; + // return new object + context.res.body = task; +} + +// Update an existing function +async function updateTask(context) { + // Grab the id from the URL (stored in bindingData) + const id = context.bindingData.id; + // Get the task from the body + const task = context.req.body; + // Update the item in the database + const result = await TaskModel.updateOne({ _id: id }, task); + // Check to ensure an item was modified + if (result.nModified === 1) { + // Updated an item, status 204 (empty update) + context.res.status = 204; + } else { + // Item not found, status 404 + context.res.status = 404; + } +} diff --git a/api/tasks/sample.dat b/api/tasks/sample.dat new file mode 100644 index 0000000..2e60943 --- /dev/null +++ b/api/tasks/sample.dat @@ -0,0 +1,3 @@ +{ + "name": "Azure" +} \ No newline at end of file diff --git a/public/index.css b/public/index.css new file mode 100644 index 0000000..e561720 --- /dev/null +++ b/public/index.css @@ -0,0 +1,12 @@ +body { + font-family: Verdana, Geneva, Tahoma, sans-serif; + font-size: 16px; +} + +.completed { + text-decoration: line-through; +} + +ul { + list-style-type: none; +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..c717bb2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,30 @@ + + + + + Tasks + + + + + + + + + +
+

My tasks

+ + + +
+ +
+
+ +
+
+ + + + diff --git a/public/local-index.js b/public/local-index.js new file mode 100644 index 0000000..42e7d44 --- /dev/null +++ b/public/local-index.js @@ -0,0 +1,135 @@ +// run the code +main(); + +// stores all tasks +const tasks = []; + +// entry function +async function main() { + await loadTasks(); + displayTasks(); +} + +// Calls server to retrieve all tasks +async function loadTasks() { + try { + // Uses fetch to call server + const response = await fetch('/api/tasks'); + // Reads returned JSON, which contains one property called tasks + const retrievedData = await response.json(); + // Retrieve tasks, which contains an array of all tasks in database + const retrievedTasks = retrievedData.tasks; + // Loop through all tasks + for (let task of retrievedTasks) { + // Add each task to the array + tasks.push(task); + } + } catch { + // If there is an error, display a generic message on the page + const messageElement = document.createElement('li'); + messageElement.innerHTML = "Could not pull data. Make sure you've configured the database." + document.getElementById('task-list').appendChild(messageElement); + } +} + +// Displays all tasks on page (called on load) +function displayTasks() { + // Loop through all tasks in the local array + for (let task of tasks) { + // Call helper function to add to UI + addTaskToDisplay(task); + } +} + +// Helper function to add task to UI +function addTaskToDisplay(task) { + // Create li element to store task display + const taskElement = document.createElement('li'); + + // create checkbox + const taskCompleteCheckbox = document.createElement('input'); + taskCompleteCheckbox.type = 'checkbox'; + // Set the ID of the checkbox to the id of the task + taskCompleteCheckbox.id = task._id; + // Set the checked property to the completion status of the task + // (Completed tasks will be checked) + taskCompleteCheckbox.checked = task.completed; + // Add event handler to each item for clicking on the checkbox + // When the checkbox is clicked, we will make a server call to toggle completed + taskCompleteCheckbox.addEventListener('change', updateTask); + // Add checkbox to li created earlier + taskElement.appendChild(taskCompleteCheckbox); + + // Create label for the task + const taskLabel = document.createElement('label'); + // Set the for attribute so the checkbox is toggled when the label is clicked + taskLabel.setAttribute('for', task._id); + // Set the text to the title of the task + taskLabel.innerText = task.title; + // Set the completed CSS class if the task is completed + taskLabel.className = task.completed ? 'completed' : ''; + // Add the label to the end of the li element + taskElement.appendChild(taskLabel); + + // Get the ul element from the page + const taskListElement = document.getElementById('task-list'); + // Add the new task to the list on the page + taskListElement.appendChild(taskElement); +} + +// Event listener for checkbox change +async function updateTask(e) { + // Get the ID of the task + const taskId = e.target.id; + + // Find the task from the array + const task = tasks.find(t => t._id === taskId); + // If no task is found (shouldn't happen), just return + if (!task) return; + // Toggle completed status + task.completed = !task.completed; + + // Get the label for the task, which contains the string + // We find this by using nextSibling + // It is next to the checkbox, which is in e.target + const taskLabel = e.target.nextSibling; + // Set the class for the task based on completed status + taskLabel.className = task.completed ? 'completed' : ''; + // Call the server to save the changes + await fetch( + `/api/tasks/${taskId}`, // URL of the API + { + method: 'PUT', // method to modify items + body: JSON.stringify(task), // put task in body + headers: { + 'Content-Type': 'application/json' // indicate return type of JSON + } + } + ); +} + +// Event listener for new tasks being created +document.getElementById('task-register').addEventListener('click', async () => { + // Create task by retrieving text from textbox + const task = { + title: document.getElementById('task-title').value + }; + // Call server + const response = await fetch( + '/api/tasks', // API location + { + method: 'POST', // POST to create new item + body: JSON.stringify(task), // Add task to body + headers: { + 'Content-Type': 'application/json' // Set return type to JSON + } + } + ); + + // Get the task returned from the server (it will have a new id) + const loadedTask = await response.json(); + // Add the task to the array for local storage + tasks.push(loadedTask); + // Add the new task to the display + addTaskToDisplay(loadedTask); +});