From 94f97046b9723f419559e8951c309b4ae2240a06 Mon Sep 17 00:00:00 2001 From: William Wu <74469686+wllmwu@users.noreply.github.com> Date: Fri, 3 Nov 2023 22:20:05 -0700 Subject: [PATCH] Address issues from beta testing (#34) * Update writeup and some code * Remove React ESLint plugin from backend * Update writeup * Add note about navigation * Adjust icon size * Run `npm audit fix` --- backend/.eslintrc.cjs | 8 +- backend/src/validators/task.ts | 57 ++++++-- frontend/package-lock.json | 202 +++++++++++++------------- frontend/src/api/tasks.ts | 12 ++ frontend/src/components/Button.tsx | 13 +- writeup/.prettierrc.json | 3 + writeup/Conclusion.md | 11 +- writeup/Introduction.md | 2 + writeup/images/contents-icon.png | Bin 0 -> 4406 bytes writeup/part-1/1-1-Task-list.md | 25 +++- writeup/part-1/1-2-Task-checkoff.md | 28 ++-- writeup/part-1/1-3-Pull-request.md | 10 +- writeup/part-2/2-1-Users.md | 22 ++- writeup/part-2/2-2-Task-detail.md | 8 +- writeup/part-2/2-3-Task-assignment.md | 18 ++- 15 files changed, 256 insertions(+), 163 deletions(-) create mode 100644 writeup/.prettierrc.json create mode 100644 writeup/images/contents-icon.png diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs index 25be971..ece0f7f 100644 --- a/backend/.eslintrc.cjs +++ b/backend/.eslintrc.cjs @@ -3,11 +3,7 @@ module.exports = { browser: true, es2021: true, }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - ], + extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], parser: "@typescript-eslint/parser", parserOptions: { ecmaVersion: "latest", @@ -16,7 +12,7 @@ module.exports = { tsconfigRootDir: __dirname, }, ignorePatterns: [".eslintrc.cjs"], - plugins: ["@typescript-eslint", "react", "no-relative-import-paths"], + plugins: ["@typescript-eslint", "no-relative-import-paths"], rules: { "default-case": "off", "no-plusplus": "off", diff --git a/backend/src/validators/task.ts b/backend/src/validators/task.ts index 52efabd..810924a 100644 --- a/backend/src/validators/task.ts +++ b/backend/src/validators/task.ts @@ -1,27 +1,56 @@ import { body } from "express-validator"; -// establishes a set of rules that the body of the post route must follow -export const createTask = [ +// more info about validators: +// https://express-validator.github.io/docs/guides/validation-chain +// https://github.com/validatorjs/validator.js#validators + +const makeIDValidator = () => + body("_id") + .exists() + .withMessage("_id is required") + .bail() + .isMongoId() + .withMessage("_id must be a MongoDB object ID"); +const makeTitleValidator = () => body("title") // title must exist, if not this message will be displayed .exists() - .withMessage("A title is required.") + .withMessage("title is required") // bail prevents the remainder of the validation chain for this field from being executed if // there was an error .bail() - .notEmpty() - .withMessage("title cannot be empty.") - .bail() .isString() - .withMessage("title must be a string.") - // escape replaces potentially-dangerous characters with HTML entities - .escape(), + .withMessage("title must be a string") + .bail() + .notEmpty() + .withMessage("title cannot be empty"); +const makeDescriptionValidator = () => body("description") - // order matters for the validation chain, by marking this field as optional, the subsequent - // parts of the chain will only be evaluated if it exists + // order matters for the validation chain - by marking this field as optional, the rest of + // the chain will only be evaluated if it exists .optional() - .escape() .isString() - .withMessage("description must be a string."), - body("isChecked").optional().isBoolean().withMessage("isChecked must be a boolean."), + .withMessage("description must be a string"); +const makeIsCheckedValidator = () => + body("isChecked").optional().isBoolean().withMessage("isChecked must be a boolean"); +const makeDateCreatedValidator = () => + body("dateCreated").isISO8601().withMessage("dateCreated must be a valid date-time string"); +// assignee is for Part 2.1 +const makeAssigneeValidator = () => + body("assignee").optional().isMongoId().withMessage("assignee must be a MongoDB object ID"); + +// establishes a set of rules that the body of the task creation route must follow +export const createTask = [ + makeTitleValidator(), + makeDescriptionValidator(), + makeIsCheckedValidator(), +]; + +export const updateTask = [ + makeIDValidator(), + makeTitleValidator(), + makeDescriptionValidator(), + makeIsCheckedValidator(), + makeDateCreatedValidator(), + makeAssigneeValidator(), // for Part 2.1 ]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 044ba7e..e3dab7a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -79,11 +79,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -168,11 +169,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -299,20 +300,20 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -462,9 +463,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -504,12 +505,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -517,9 +518,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2007,31 +2008,31 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2040,12 +2041,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -14823,9 +14824,9 @@ } }, "node_modules/postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "funding": [ { "type": "opencollective", @@ -21014,11 +21015,12 @@ } }, "@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -21078,11 +21080,11 @@ } }, "@babel/generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", - "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -21176,17 +21178,17 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -21288,9 +21290,9 @@ "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.22.5", @@ -21318,19 +21320,19 @@ } }, "@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", - "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.22.5", @@ -22286,39 +22288,39 @@ } }, "@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.22.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", - "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", - "requires": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.7", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.7", - "@babel/types": "^7.22.5", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -31604,9 +31606,9 @@ } }, "postcss": { - "version": "8.4.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.26.tgz", - "integrity": "sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "requires": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", diff --git a/frontend/src/api/tasks.ts b/frontend/src/api/tasks.ts index 0071fa7..e57a994 100644 --- a/frontend/src/api/tasks.ts +++ b/frontend/src/api/tasks.ts @@ -57,6 +57,18 @@ export interface CreateTaskRequest { description?: string; } +/** + * The expected inputs when we want to update an existing Task object. Similar to + * `CreateTaskRequest`. + */ +export interface UpdateTaskRequest { + _id: string; + title: string; + description?: string; + isChecked: boolean; + dateCreated: Date; +} + /** * The implementations of these API client functions are provided as part of the * MVP. You can use them as a guide for writing the other client functions. diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/Button.tsx index e2411c6..288a29e 100644 --- a/frontend/src/components/Button.tsx +++ b/frontend/src/components/Button.tsx @@ -33,20 +33,23 @@ export interface ButtonProps extends React.ComponentProps<"button"> { * with our own styling and restrictions on what can be put inside of it. */ export const Button = React.forwardRef(function Button( - { label, kind = "primary", ...props }, + { label, kind = "primary", className, ...props }, ref, ) { - let className = styles.button; + let buttonClass = styles.button; switch (kind) { case "primary": - className += ` ${styles.primary}`; + buttonClass += ` ${styles.primary}`; break; case "secondary": - className += ` ${styles.secondary}`; + buttonClass += ` ${styles.secondary}`; break; } + if (className) { + buttonClass += ` ${className}`; + } return ( - ); diff --git a/writeup/.prettierrc.json b/writeup/.prettierrc.json new file mode 100644 index 0000000..de753c5 --- /dev/null +++ b/writeup/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 100 +} diff --git a/writeup/Conclusion.md b/writeup/Conclusion.md index 5d00cb0..872f984 100644 --- a/writeup/Conclusion.md +++ b/writeup/Conclusion.md @@ -8,12 +8,13 @@ That's a wrap for onboarding. We're excited to see what you create next! ## Extra credit -Want to challenge yourself and explore further? In no particular order, here are some ideas for additional features and improvements: +Want to challenge yourself and explore further? Here are some ideas for additional features and improvements: -- **User accounts and authentication** [large size, hard difficulty]: Add the ability to sign up, sign in, and sign out. Make each task only visible to the user who created it. +- **Automatic refresh** [small size, easy difficulty]: Make the `TaskList` on the Home page refresh itself after a new task is successfully created through the `TaskForm`. - **Automated tests** [small size, easy difficulty]: Add more unit tests to properly cover `TaskForm` and `TaskItem`. -- **Task search, sorting, and filters** [medium size, medium difficulty]: Add the ability to search, sort, and filter tasks (search title/description, sort by title/creation date/status, filter by status, etc.). +- **Task deletion** [small size, easy difficulty]: Add the ability to delete tasks from the frontend. - **Better user selection** [medium size, easy difficulty]: Replace the "Assignee ID" text field in the `TaskForm` with a dropdown menu that allows you to choose a user by name. +- **Task search, sorting, and filters** [medium size, medium difficulty]: Add the ability to search, sort, and filter tasks (search title/description, sort by title/creation date/status, filter by status, etc.). - **CSV import/export** [medium size, medium difficulty]: Add the ability to upload and download tasks in CSV format. -- **Task deletion** [small size, easy difficulty]: Add the ability to delete tasks from the frontend. -- **Task due dates** [medium size, medium difficulty]: Add the ability to set a due date on each task and to sort tasks by due date. +- **Task due dates** [medium size, hard difficulty]: Add the ability to set a due date on each task and to sort tasks by due date. +- **User accounts and authentication** [large size, hard difficulty]: Add the ability to sign up, sign in, and sign out. Make each task only visible to the user who created it. diff --git a/writeup/Introduction.md b/writeup/Introduction.md index fb15e2b..1a6efd9 100644 --- a/writeup/Introduction.md +++ b/writeup/Introduction.md @@ -1,5 +1,7 @@ # Introduction +**Note:** We recommend viewing these writeup files on GitHub. You can quickly navigate the headings in each part by clicking on this icon at the top of the file: + ## Project overview We want to build a simple todo list app to keep track of tasks. At a high level, the app should be able to do the following things: diff --git a/writeup/images/contents-icon.png b/writeup/images/contents-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..484fc047f7c5584e3496ed0ba7664e2e929ec7c4 GIT binary patch literal 4406 zcmZ`+1z1$u7Cu8WLnArFP=cs1bay!@3OGoE#DE|@)F3D+ofiZo6#>zqrBkF+8U%z( zNJt2Rr0@py-RpPXefxa-thLu#|BAiW{=RdFdcHzB;`Qb<3+3CYA1=~~f zr|HxOTR^sFAQ#Qjvo&B50%>#Q0PQA@h)ShTH%Ue44SS_ zSo9IDk6E^oCb?TnVNUpph>&;GwxDbu(w$vSC;AcXjnD~1CoKs`J4a;u$g_$=) zq<5-s8u6zt7SA;c&c)AcsTHO`;w}6jOh>blTU5s>me~zr0Jl{nw&Z2wg|k?ygaw6Q zbg04dT(_xuA2SwFxxdPe%K%E*S|NvoO55PHzIlWz*!-SKUP>GCi>;Jps(C^2al*7} zp7NQ8cvj*24rF9GZ30K0qJs|iwTM6iI?Im0=>wOLzLa@~J0o$?1~k0Uvv~88jc{(o zqF6dqM9{*8Z>U2;m9K5$wvY)OaD5Y8h69<&j*ae#Z=+leG@XZ}hO0=Q1SW<(tNC&e zptMtCHE__<>K)QgtYe{=?tA4X^->$(x~s@Tv#>;opoxlXJ~JWuTl~Qg-J)3A2(T5+ zqU(H{RC);`M)p9M3StR^0Z0-iiNo)N#^j)yLQ` zald-m_6*BjHW1`^Fz{qTc^w#w$!P7j&?r$??1-__G9dOuMuh5*TTQMp@6hu`@-nQE zoc`&&(xoY4IEVZ^@_AS7qkd-=zn=#F826xPDeQ7xTz{}Rfqcf*Md_FqGBQ$i0PK5X zr@6VaC2E#jHGe@yVv7>+jiIEVU_0#(VI{tsMLd1lH!B4tI&tU2cd1OhOwG_GW#Yrp zTouebZFkhnn1TRvhID~Me@AMy=#^K5*^=0qXZ-V5kvMn*_v`%uY^ z8&Q2N-6dcw^Xqp;bLhrE4=cY3jr6c#7wJtv--RzT487fTaK%@sk!?0yV$Sh^?gaEP z_7Rsh>0^QifeZm!*Ep=H^QbgQ>4G^u}Pe!EIkY(nOYSlWrqT)+Ny; zfu-oOva-~&d^c>FA|__XXsMywHi@0@5pob&dfTEP>UR9gD>{?U+?1D|bVK=*?A7-2 z^K>8u*I$Z_+#Wd|p?oE(r=eHLf6JrNKG!~I4Kw?dzyPTd(bs}X3^+D=0WB|=14J5yYyl2JjBqOA+n)GyZ-U%R~XH&)Z{s- z!v>-Pj%<$54bei4$d{H|URG2)A}Xy7Y>bOa9jbhtQI1It70s^(*HSAo#inW} zYR8U>$Vb};Y|6AM`t*#SUwtP&?isYfBQw(9CookswE0}le#=R)WcKc?SOR3q}aCg$c;`exJ^3#)jowR_LX zc)w@A$+G71YXM~P-Q-CDErDW_4b#ISsHt=9;56q9eRV~xpU1unL*01IzK?i{XYjv@MC5ETHz4cDJd^nU!XB}ZV4v`jm-u4wwauGMNx~va%CD4O!746ACq7B6 zlyki6@P1;{>Op%=^kr5rzxus{f`tJscuJhapDU^r;pySTL!_%+t93(7F3+V#(*wvn z)%DaBlG{w*`Mv$RTsXhC<9BfR;FgG=u)ei?&rX+7>S)?VvT6Jb?_JU{h}lx`Cx>oK z7Br)d%L^_jRV;6QNkOj3@a3gmx=JnKOi>Tri35K zA%*U_VtuC_J2v^Le0g9ql~&kx*`#}Vx~3^vG6~CvkrBk0_4wu-qCY7d@hm2P&>huv zU5zIE2ob;9$^fSK6mTWwh0zJ~v_q z%pMgAXO}g0m)Pr!9+TITCrcbTW2da&6!;G3_XYMzR0TWOy{vpyG@&_d^k%QC@`d9@ zlhDK0lTIZKzQ^ONEvz##3!asu-XkOH!qXLR=nIoZzHPEq9S>zT%V~K~co?sFto7^% zHY79*?B{OPH7fW%`V49hs115T{F-8rB1sHJKFoXsEr-^n>88~_efQMZxZaq8)l;^9 zA#63iJLiK&GFuKy+9QWA-rL&&w9je%c*}3f+h^^{tb!fYx!*`;bd^Qa->`0@Vhvy$&Q zjZ+QBD?!b~tdtAvA2F?dyYsGZ7X;_PIGIqUaJ-{p#1i;2tyALg9dbQq6U72*rw@6RqZ9iKF=2c3pyurskw zDeye3I-L=!2>EKuiDE@@KNL?;1_&E1Er1`#lK_Mu z8UP>1fp8B1Vg?9*@Bp9*g8s$pg7|*f-~m9W9YFBQ#t28xM;vbA(7z~t!ef9Kx1zz# z;B36Vtl`=CfAcsUpmbXWfxyw*R&FSi6Z)RBdv&w=F-}10a?KD80Q5ZP6NJ#^{sI7a zqjpFGcLS{((pJun0v6V|?<@gtN0)OyfULJPj&(%2Tfn>>9h}h8-g2BjETnP#xmb`B z_QS;ez8t54mNrbq*$o8~7k~@EIpx7H7);j9+D2MeRs9znx02($=kD$zEhy;a&pc7yc7I25LjNiYSD@gzMo>rqF8FUScRQQ^13TCJg#D=NXE@n&XVThs z-Y5qHRXa!AR^w8W7ZaEL5#@grf9LrVsE0GR)HC&a)Hq6&E~E{5TN*W*ADn*f5$Wl*XcEa6)u}lx40&!r#w*AK0m5^| zJU~O@vB!O8J135-LPE-vV&NR6Z$53yW~=E9N_#-`b{|Pzf%sDgg3|RBdND_C7Y=;sIv|_GTyDf_wRp}!A!Ttmj$}q)= k3~)2aTL1|VkkmDS2;V%~JyH*Nc7EFtYByDjl` to use flexbox layout, have its content axis in the row direction (so its children will be laid out side-by-side), center its children on the cross axis (so they will be vertically centered), and add a gap of 0.25rem (4px) between children. Refer to the [CSS-Tricks flexbox guide](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) or the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/CSS/flex-direction) for more information on each property and its possible values. 2. Add another CSS class for the **inner** `
`, which contains the title and description labels. It should be similar to the previous class, but we want the children to be laid out in the column direction (vertical), to be centered vertically, and to stretch out as much as possible horizontally. We also want the `
` itself to take up all remaining space in the parent `
` and to have a bottom border. You can copy and fill in the template below. + ```css .textContainer { + height: 100%; flex-grow: ???; - border-bottom: 1px solid var(--color-text-secondary); display: flex; flex-direction: ???; justify-content: ???; align-items: ???; + overflow: hidden; + } + + .textContainer span { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } ``` + Remember to add the new `className` prop in `TaskItem.tsx` as well. +
+ ❓ Hint: Truncating overflowing text + + _It can be surprisingly complicated to truncate overflowing text using only CSS. The `.textContainer span` styles in the above code block achieve this in combination with the `overflow: hidden;` on `.textContainer`. This may come in handy when implementing other components! (Credit to this [CSS-Tricks snippet](https://css-tricks.com/snippets/css/truncate-string-with-ellipsis/) and this particular [comment](https://css-tricks.com/snippets/css/truncate-string-with-ellipsis/#comment-1607839).)_ +
+ 3. Add two more CSS classes, one for the title label and one for the description label. For both, you only need to set the font property using one of the app fonts in `globals.css`. The `TaskItem` title uses the label font and the description uses the body font. Here's CSS for the title: ```css .title { @@ -290,7 +308,8 @@ _In a real project, we could use a route like this to search and sort/filter Tas 10. Add some CSS classes to `TaskList.module.css` and add the corresponding `className` props to `TaskList.tsx`. 1. We need one class for the list title, which uses the heading font. This works similarly to the title and description classes from `TaskItem`. 2. We need another class for the inner `
`, which is the item container. Use flexbox again to align its children: column direction, horizontally stretched. The item container itself should also have `width: 100%`. -11. Check the Home page again. You should see all the Tasks that you've created so far, matching the Figma design. Submit some more through the "New task" form and refresh the page. The new Tasks should appear in the list. Again, if something's not working and you can't figure it out, ping us in **#onboarding** on Slack. + 3. Finally, we need a class for the outermost list container `
`. This just needs a top margin of 3rem. +11. Check the Home page again. You should see all the Tasks that you've created so far, matching the Figma design. Submit some more through the "New task" form (making sure to test things like super long titles and descriptions) and refresh the page. The new Tasks should appear in the list. Again, if something's not working and you can't figure it out, ping us in **#onboarding** on Slack.
✅ Good practice: List element instead of generic div diff --git a/writeup/part-1/1-2-Task-checkoff.md b/writeup/part-1/1-2-Task-checkoff.md index b52bc10..9ae2a55 100644 --- a/writeup/part-1/1-2-Task-checkoff.md +++ b/writeup/part-1/1-2-Task-checkoff.md @@ -80,17 +80,18 @@ _Here we just replace the entire Task object with the provided data, even the `d 4. Use the [`Model.findByIdAndUpdate()`]() Mongoose function to update the Task in the database with the given ID. (Actually, there are several functions you can use; any approach that works is valid.) 1. Remember to `await` the returned `Query`. 2. If the returned `Query` gives us null, then there was no object in the database with that ID. In that case, return a 404 response. - 3. Otherwise, return a 200 response containing the updated Task. -5. Test your implementation. Make sure your backend is running locally, then call the new route through Postman or run the following command with your own values filled in: + 3. Otherwise, return a 200 response containing the updated Task. The result of `findByIdAndUpdate` is the original Task, so you should execute a new query like in `getTask`. +5. Add the new route to `src/routes/task.ts`. Similar to `createTask`, use the `updateTask` validation chain provided in `src/validators/task.ts`. +6. Test your implementation. Make sure your backend is running locally, then call the new route through Postman or run the following command with your own values filled in: ```shell - curl -X "POST" http://127.0.0.1:3001/api/task/ \ + curl -X "PUT" http://127.0.0.1:3001/api/task/ \ -H "Content-Type: application/json" \ - -d '{\"_id\":\"\",\"title\":\"Your title\",\"description\":\"Your description\",\"isChecked\":false,\"dateCreated\":\"2023-10-01T00:00Z\"}' + -d '{"_id":"","title":"","description":"","isChecked":false,"dateCreated":"2023-10-01T00:00Z"}' ``` You should see the Task updated with its new data when you list all Tasks in mongosh and when you view the frontend Home page. -6. Copy the skeleton code below into `frontend/src/api/tasks.ts`: +7. Copy the skeleton code below into `frontend/src/api/tasks.ts`: ```typescript - export async function updateTask(task: Task): Promise> { + export async function updateTask(task: UpdateTaskRequest): Promise> { try { // your code here } catch (error) { @@ -98,7 +99,7 @@ _Here we just replace the entire Task object with the provided data, even the `d } } ``` -7. Using the existing functions as guides, complete the implementation of `updateTask`. +8. Using the existing functions as guides, complete the implementation of `updateTask`. ## Update to component: `TaskItem` @@ -117,7 +118,7 @@ _Here we just replace the entire Task object with the provided data, even the `d import React, { useState } from "react"; // update this line // ... export function TaskItem({ task: initialTask }: TaskItemProps) { - // update this line + // update the previous line and add the following const [task, setTask] = useState(initialTask); const [isLoading, setLoading] = useState(false); @@ -125,23 +126,28 @@ _Here we just replace the entire Task object with the provided data, even the `d // your code here }; - // return... + // ... } ``` -2. Within `handleToggleCheck`, set `isLoading` to true (by calling `setLoading`—never assign directly to a state variable, because React will ignore it), then call the `updateTask` function (be sure to import it from `src/api/tasks`). Pass in the task from the `TaskItem` props, with the value of `isChecked` flipped. +2. Within `handleToggleCheck`, set `isLoading` to true (by calling `setLoading`—never assign directly to a state variable, because React will ignore it), then call the `updateTask` function (be sure to import it from `src/api/tasks`). Pass in the `task` state variable, with the value of `isChecked` flipped.
🤔 For new developers: Spread syntax _An easy way to do this is to use JavaScript's [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#spread_in_object_literals). You can write something like `{ ...task, isChecked: !task.isChecked }`. This is preferable because it's concise and it creates a (shallow) copy of `task`; we shouldn't modify `task` or any other props directly because that might cause unintended side effects._
-3. When `updateTask` resolves, call `setTask` with the new task from the response and set `isLoading` back to false. See the `handleSubmit` function in `components/TaskForm.tsx` for an example of how to handle the result of a request (the request is `createTask` in that case). +3. When `updateTask` resolves, call `setTask` with the new task from the response (or `alert()` the user again if it failed) and set `isLoading` back to false. See the `handleSubmit` function in `components/TaskForm.tsx` for an example of how to handle the result of a request (the request is `createTask` in that case).
🤔 For new developers: await or async _If you make `handleToggleCheck` an `async` function, you can use `await` syntax instead of `then()`. There's no real difference here, so it's up to preference. Just stay consistent and don't mix the two syntaxes together._
+
+ ❓ Hint: Where to call setLoading + + _Make sure you call `setLoading(false)` **inside** the `.then()` block. If it's outside, then it will run immediately instead of after the response-handling code finishes._ +
4. Pass two props to the `CheckButton`: `onPress` and `disabled`. We want `handleToggleCheck` to be called when the `CheckButton` is pressed, and we want the `CheckButton` to be disabled when `isLoading` is true.
diff --git a/writeup/part-1/1-3-Pull-request.md b/writeup/part-1/1-3-Pull-request.md index ed6f454..55c2292 100644 --- a/writeup/part-1/1-3-Pull-request.md +++ b/writeup/part-1/1-3-Pull-request.md @@ -1,10 +1,7 @@ # 1.3. Make a pull request 1. Open your repository in GitHub (your fork, not the original). -2. Create a pull request from your `part-1` branch into `main`. There are several ways to get there: - 1. If you pushed changes recently, you'll see a banner at the top of the page with a button to "Create pull request." Click that button. - 2. Alternatively, open the `part-1` branch by selecting it from the branch selection menu, then click "Create pull request." - 3. Alternatively, click the "Pull requests" tab at the top of the page, then click "New pull request." Select the `part-1` branch in the second dropdown menu, then click "Create pull request." +2. [Start a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request#creating-the-pull-request) from your `part-1` branch into `main`. 3. Scroll through the diff (difference comparison) to make sure you haven't accidentally left in debugging code, TODO comments, etc. If you have, remove them and push a new commit.
✅ Good practice: Fix problems before review @@ -31,6 +28,11 @@ _There are [multiple ways to merge a PR](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github) on GitHub: merge commit, squash-and-merge, or rebase-and-merge. We generally recommend that you do one of the latter two in order to keep the `main` commit history clean. Your engineering manager will decide the policy for your project._
+
+ ✅ Good practice: Enforce these practices with your repo settings + + _You can enable or disable each PR merge method in your repository settings, as well as set [branch protection rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule) to, say, require automatic checks to pass before a PR can be merged into the `main` branch. These options are set in the template repo, but they're not copied to repos created from it. Your engineering manager should handle the settings on your future project repository._ +
7. Back in your command prompt, checkout and pull the `main` branch to update it on your local computer: ```shell diff --git a/writeup/part-2/2-1-Users.md b/writeup/part-2/2-1-Users.md index 6d9a532..ef07072 100644 --- a/writeup/part-2/2-1-Users.md +++ b/writeup/part-2/2-1-Users.md @@ -62,7 +62,7 @@ Creates a new User object in the database with the fields provided in the reques 3. Create a new file `backend/src/validators/user.ts`. 4. Use `createTask` from `validators/task.ts` as a guide to define a new `express-validator` validation chain. ```typescript - export const createTask = [ + export const createUser = [ // ... ]; ``` @@ -74,7 +74,7 @@ Creates a new User object in the database with the fields provided in the reques ```shell curl -X "POST" http://127.0.0.1:3001/api/user \ -H "Content-Type: application/json" \ - -d '{\"name\":\"Some name\",\"profilePictureURL\":\"Some URL to an image\"}' + -d '{"name":"","profilePictureURL":""}' ``` Feel free to use the example profile pictures that we've provided in the `frontend/public` folder. The URLs for these would be, for example, `/profile1.png` (including the leading slash). @@ -113,12 +113,13 @@ Returns the User object with the provided ID. If no such User exists in the data ### Walkthrough 1. In `backend/src/controllers/user.ts`, add another request handler `getUser`. Use `getTask` from `controllers/task.ts` as a guide. -2. Test your implementation by using Postman or running the following command with some different IDs: +2. Add the new route to `src/routes/user.ts`. +3. Test your implementation by using Postman or running the following command with some different IDs: ```shell curl http://127.0.0.1:3001/api/user/ ``` Try a couple User IDs that you got from mongosh, as well as some Task IDs and some nonexistent IDs. -3. In `frontend/src/api/users.ts`, add an API client function that makes requests to this route. Use `getTask` from `api/tasks.ts` as a guide. +4. In `frontend/src/api/users.ts`, add an API client function that makes requests to this route. Use `getTask` from `api/tasks.ts` as a guide. ## Update to schema: `Task` @@ -132,7 +133,9 @@ The `Task` schema should have the following additional fields: ### Walkthrough 1. In `backend/src/models/task.ts`, update the `Task` schema with the new `assignee` field. Be sure to use the [ObjectId type](https://mongoosejs.com/docs/schematypes.html#objectids) from Mongoose and make it a [`ref`](https://mongoosejs.com/docs/populate.html) to the `'User'` schema. -2. Update all API routes that return a Task object to populate the `assignee` field with the corresponding User object. This means Mongoose will automatically replace the ID with the actual object in the return value, or null if it doesn't exist. You should update `getTask`, `createTask`, and `getAllTasks` in `backend/src/controllers`. +2. Update all API routes that return a Task object to [populate](https://mongoosejs.com/docs/populate.html#population) the `assignee` field with the corresponding User object. This means Mongoose will automatically replace the ID with the actual object in the return value, or null if it doesn't exist. You should update `getTask`, `createTask`, `updateTask`, and `getAllTasks` in `backend/src/controllers`. + + 1. For `createTask`, you may need to run a query for the newly created Task so you can populate it.
🤔 For new developers: Populating objects @@ -141,10 +144,15 @@ The `Task` schema should have the following additional fields: 3. In `frontend/src/api/tasks.ts`, add the following field to both the `Task` and `TaskJSON` interfaces: ```typescript - assignee: User; + assignee?: User; ``` where `User` is imported from `api/users.ts`. -4. Update the `parseTask` function to include the assignee. +4. In the same file, add the following field to `CreateTaskRequest` and `UpdateTaskRequest`: + ```typescript + assignee?: string; + ``` + This is a string instead of a `User` because we only want to send the ID, not the entire User object. +5. Update the `parseTask` function to include the assignee.
⚠️ Caution: Updating validators diff --git a/writeup/part-2/2-2-Task-detail.md b/writeup/part-2/2-2-Task-detail.md index 3948e90..3974f3c 100644 --- a/writeup/part-2/2-2-Task-detail.md +++ b/writeup/part-2/2-2-Task-detail.md @@ -26,7 +26,13 @@ Next, we'll add a new frontend page that displays information about a particular 6. Within the `TaskDetail` component, add a state variable `task` which will store the Task object and add a `useEffect` hook which retrieves the task and puts it into that state variable. 1. Use the [`useParams` hook](https://reactrouter.com/en/main/hooks/use-params) from React Router to get the task ID from the URL (this is called a [dynamic segment](https://reactrouter.com/en/main/route/route#dynamic-segments)). This ID should be the only dependency of the `useEffect` hook. 2. Use the `getTask` function from `frontend/src/api/tasks.ts` to retrieve the task from the backend. -7. Add the various text components, Home page link, and "Edit task" button as shown in the Figma. Be sure to style them correctly—you can add a new CSS file `pages/TaskDetail.module.css` for font and layout styling—and to conditionally render things as needed. +7. Add the various text components, Home page link, and "Edit task" button as shown in the Figma. Be sure to style them correctly—you can add a new CSS file `pages/TaskDetail.module.css` for font and layout styling—and to conditionally render things as needed. Also, use the `Helmet` component to set the page title as specified above. +
+ ❓ Hint: Date-time formatting + + _We recommend using the JavaScript built-in class [`Intl.DateTimeFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) to format dates and times consistently. In this case, for the task creation date, you can use the `"en-US"` locale, `"full"` date style, and `"short"` time style._ +
+ 8. Verify that different tasks from your local database are displayed correctly on the page. Make sure to check special cases like overflowing text, empty description, and nonexistent ID. ## Update to component: `TaskItem` diff --git a/writeup/part-2/2-3-Task-assignment.md b/writeup/part-2/2-3-Task-assignment.md index 045c89c..352b54a 100644 --- a/writeup/part-2/2-3-Task-assignment.md +++ b/writeup/part-2/2-3-Task-assignment.md @@ -17,18 +17,20 @@ Finally, we'll add the ability to assign users to tasks and see who is the assig 1. Export the user icon from Figma as an SVG. 1. Right-click on the icon, choose "Select layer > User icon", then click "Export User icon" in the Export panel of the right sidebar. + 2. Save the SVG in the `frontend/public` folder with a name like `userDefault.svg`. 2. In `frontend/src/components`, create two new files: `UserTag.tsx` and `UserTag.module.css`. 3. Write the `UserTag` component. It should be a "pure" component (no state or side effects). Make sure you cover all the possible cases with conditional rendering. - 1. Use an [`` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) to display the profile picture. + 1. Use an [`` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img) to display the profile picture. To use the default icon we just downloaded, pass in `src="/userDefault.svg"` (or whatever file name you used).
❓ Hint: CORS errors _You might encounter a CORS (Cross-Origin Resource Sharing) permission error caused by the profile picture. This happens because browsers block requests for any resource that's not from the same origin (in our case, `localhost:3000`) by default for security reasons. You can ignore these errors and just use the example profile pictures that we provided (see [Part 2.1](./2-1-Users.md))._
-4. Add styles to the `UserTag` component. You should just need some layout styling, font styling for the name label, and a border-radius on the profile picture. -5. **Optional:** Add a `className` prop and pass it along to the outermost element in your `UserTag` JSX. For example, if you used a `
` as the outermost element, then write `
`. You can use this prop to make layout easier in the next steps. +4. Add styles to the `UserTag` component. You should just need some layout styling and a `border-radius` on the profile picture. +5. Export `UserTag` from `components/index.ts`. +6. **Optional:** Add a `className` prop and pass it along to the outermost element in your `UserTag` JSX. You can use this prop to make layout easier in the next steps. ## Update to component: `TaskItem` @@ -66,15 +68,17 @@ Finally, we'll add the ability to assign users to tasks and see who is the assig [Link to updated `TaskDetail` page in Figma](https://www.figma.com/file/8eRDNyOrYRgyN7NNb0mIXA/Onboarding-Todo-App?type=design&node-id=38-575&mode=design&t=sAnv6Hgp6SzriN7g-4) +- Show a `UserTag` for the task's assignee. - When "Edit task" is clicked, display the `TaskForm` component in edit mode with the data from this task. - When the `TaskForm` is submitted, display the task information view again with the updated task data. ### Walkthrough -1. In the `TaskDetail` page component, add another state variable `isEditing` which will store a boolean indicating whether the `TaskForm` is open. -2. Set `isEditing` to true when the "Edit task" button is clicked. -3. When `isEditing` is true, display a `TaskForm` in edit mode prefilled with this task's data; otherwise, display the task information as we just implemented. Upon submission of the `TaskForm`, change `isEditing` back to false and set the `task` state variable to the updated task in the callback. -4. Test your changes by opening the `TaskDetail` page, clicking "Edit task," changing the values, and clicking Save. You can copy and paste user IDs from mongosh. Try this for multiple different tasks and try some special cases: empty form fields, a task ID instead of a user ID, a nonexistent ID, no change in the form, etc. +1. In the `TaskDetail` page component, import `UserTag` and use it to display the assignee. +2. Add another state variable `isEditing` which will store a boolean indicating whether the `TaskForm` is open. +3. Set `isEditing` to true when the "Edit task" button is clicked. +4. When `isEditing` is true, display a `TaskForm` in edit mode prefilled with this task's data; otherwise, display the task information as we just implemented. Upon submission of the `TaskForm`, change `isEditing` back to false and set the `task` state variable to the updated task in the callback. +5. Test your changes by opening the `TaskDetail` page, clicking "Edit task," changing the values, and clicking Save. You can copy and paste user IDs from mongosh. Try this for multiple different tasks and try some special cases: empty form fields, a task ID instead of a user ID, a nonexistent ID, no change in the form, etc.
🤔 For new developers: Thinking about the user experience