CMU Coding Bootcamp

Compare changes

Choose any two refs to compare.

+85
html/css/form.css
··· 1 + .container { 2 + display: flex; 3 + flex-direction: column; 4 + align-items: center; 5 + justify-content: center; 6 + } 7 + 8 + form { 9 + display: flex; 10 + flex-direction: column; 11 + align-items: center; 12 + justify-content: center; 13 + gap: 0.5rem; 14 + width: 80%; 15 + } 16 + 17 + form > * { 18 + width: 100%; 19 + } 20 + 21 + form :nth-child(even) { 22 + margin-bottom: 1rem; 23 + } 24 + 25 + form > button { 26 + width: 100%; 27 + padding: 0.25rem; 28 + margin: 0.5rem; 29 + } 30 + 31 + @media (min-width: 769px) { 32 + form { 33 + gap: 1rem; 34 + width: 100%; 35 + max-width: 48rem; 36 + display: grid; 37 + column-gap: 1rem; 38 + grid-template-columns: repeat(2, minmax(0, 1fr)); 39 + } 40 + 41 + form > button { 42 + width: 100%; 43 + grid-column: span 2; 44 + padding: 0.25rem; 45 + margin: 0.5rem; 46 + } 47 + } 48 + 49 + nav { 50 + display: flex; 51 + padding: 0.5rem 0rem; 52 + background-color: #8aacdf; 53 + position: sticky; 54 + top: 0; 55 + left: 0; 56 + right: 0; 57 + margin-left: -0.5rem; 58 + margin-top: -0.5rem; 59 + justify-content: space-around; 60 + align-items: center; 61 + width: 100vw; 62 + margin-bottom: 4rem; 63 + } 64 + 65 + .navitem { 66 + padding: 0.5rem 4rem; 67 + border: 1px solid #000; 68 + border-radius: 0.5rem; 69 + } 70 + 71 + #footer { 72 + width: 100vw; 73 + height: 6rem; 74 + position: absolute; 75 + bottom: 0; 76 + left: 0; 77 + justify-content: space-around; 78 + align-items: center; 79 + background-color: #000; 80 + color: #fff; 81 + text-align: center; 82 + display: flex; 83 + vertical-align: center; 84 + font-size: 2rem; 85 + }
+28
html/form.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Form</title> 7 + <link rel="stylesheet" href="css/form.css" /> 8 + <script src="js/form.js"></script> 9 + </head> 10 + <body> 11 + <nav> 12 + <a class="navitem" href="/">Home</a> 13 + <a class="navitem" href="/dino.html">Dino</a> 14 + </nav> 15 + <div class="container"> 16 + <form> 17 + <label for="name">Name:</label> 18 + <input type="text" id="name" name="name" required /> 19 + <label for="email">Email:</label> 20 + <input type="email" id="email" name="email" required /> 21 + <label for="message">Message:</label> 22 + <textarea id="message" name="message" required></textarea> 23 + <button type="submit">Submit</button> 24 + </form> 25 + </div> 26 + <div id="footer">Totally good footer text</div> 27 + </body> 28 + </html>
-5
nilla.nix
··· 78 78 mkShell { 79 79 shellHook = '' 80 80 [ "$(hostname)" = "shorthair" ] && export ZED_PREDICT_EDITS_URL=http://localhost:9000/predict_edits 81 - unset TMPDIR 82 81 ''; 83 82 packages = [ 84 83 (python3.withPackages (ppkgs: [ ··· 108 107 mkShell, 109 108 }: 110 109 mkShell { 111 - shellHook = '' 112 - unset TMPDIR 113 - ''; 114 110 packages = [ 115 111 pkgs.bun 116 112 pkgs.eslint_d ··· 133 129 }: 134 130 mkShell { 135 131 shellHook = '' 136 - unset TMPDIR 137 132 serve() { 138 133 live-server /home/coded/Programming/CMU/html --port 5000 139 134 }
+38 -3
react/src/App.tsx
··· 1 1 import { posts } from "./lib/post"; 2 2 import { BlogPostList } from "./components/BlogPostList"; 3 3 import { Link } from "react-router"; 4 + import { useState } from "react"; 4 5 5 6 export function App() { 7 + const [searchBarDisplay, displaySearchBar] = useState(false); 6 8 return ( 7 9 <> 8 10 <title>Posts</title> 9 11 <div className="w-screen p-5 flex flex-col items-center gap-10"> 12 + <nav className="flex justify-between items-center w-full sticky top-0"> 13 + <h1 className="text-3xl font-bold text-left">Blog App</h1> 14 + {searchBarDisplay ? ( 15 + <> 16 + <input 17 + type="text" 18 + placeholder="Search..." 19 + className="border border-gray-300 rounded px-2 py-1" 20 + onChange={(e) => { 21 + // Implement search functionality here 22 + }} 23 + /> 24 + <button 25 + className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center" 26 + onClick={() => displaySearchBar(false)} 27 + > 28 + Close 29 + </button> 30 + </> 31 + ) : ( 32 + <button 33 + className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center" 34 + onClick={() => displaySearchBar(true)} 35 + > 36 + Search 37 + </button> 38 + )} 39 + <div className="flex w-full justify-end items-center"> 40 + <Link to="/post" className="w-1/3"> 41 + <div className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center"> 42 + New Post 43 + </div> 44 + </Link> 45 + </div> 46 + </nav> 10 47 <div className="flex flex-col gap-4 md:grid md:grid-cols-3 items-center justify-between w-full"> 11 - <h1 className="text-5xl font-bold md:col-start-2 text-center w-full"> 12 - Posts 13 - </h1> 14 48 <div className="flex w-full justify-end items-center"> 49 + <h1 className="text-5xl font-bold text-center">Posts</h1> 15 50 <Link to="/post" className="w-1/3"> 16 51 <div className="bg-blue-500 hover:bg-blue-700 text-white w-full font-bold py-2 px-4 rounded cursor-pointer text-center"> 17 52 New Post
+34
server/.gitignore
··· 1 + # dependencies (bun install) 2 + node_modules 3 + 4 + # output 5 + out 6 + dist 7 + *.tgz 8 + 9 + # code coverage 10 + coverage 11 + *.lcov 12 + 13 + # logs 14 + logs 15 + _.log 16 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 + 18 + # dotenv environment variable files 19 + .env 20 + .env.development.local 21 + .env.test.local 22 + .env.production.local 23 + .env.local 24 + 25 + # caches 26 + .eslintcache 27 + .cache 28 + *.tsbuildinfo 29 + 30 + # IntelliJ based IDEs 31 + .idea 32 + 33 + # Finder (MacOS) folder config 34 + .DS_Store
+15
server/README.md
··· 1 + # server 2 + 3 + To install dependencies: 4 + 5 + ```bash 6 + bun install 7 + ``` 8 + 9 + To run: 10 + 11 + ```bash 12 + bun run index.ts 13 + ``` 14 + 15 + This project was created using `bun init` in bun v1.2.22. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+1
server/books.json
··· 1 + [{"id":"9780553212471","title":"Frankenstein","author":"Mary Shelley"},{"id":"9780060935467","title":"To Kill a Mockingbird","author":"Harper Lee"},{"id":"9780141439518","title":"Pride and Prejudice","author":"Jane Austen"}]
+288
server/books.ts
··· 1 + import express, { 2 + type NextFunction, 3 + type Request, 4 + type Response, 5 + } from "express"; 6 + import { writeFile, readFile, exists } from "fs/promises"; 7 + 8 + const ISBN13 = 9 + /^(?:ISBN(?:-13)?:? )?(?=[0-9]{13}$|(?=(?:[0-9]+[- ]){4})[- 0-9]{17}$)[\d-]+$/; 10 + 11 + interface Book { 12 + id: string; 13 + title: string; 14 + author: string; 15 + } 16 + 17 + const initBooks: () => Promise<void> = async () => { 18 + await writeFile( 19 + "books.json", 20 + JSON.stringify([ 21 + { 22 + id: "9780553212471", 23 + title: "Frankenstein", 24 + author: "Mary Shelley", 25 + }, 26 + { 27 + id: "9780060935467", 28 + title: "To Kill a Mockingbird", 29 + author: "Harper Lee", 30 + }, 31 + { 32 + id: "9780141439518", 33 + title: "Pride and Prejudice", 34 + author: "Jane Austen", 35 + }, 36 + ]), 37 + ); 38 + }; 39 + 40 + enum ErrorType { 41 + NotFound, 42 + InvalidId, 43 + BadData, 44 + AlreadyExists, 45 + } 46 + 47 + class BookError extends Error { 48 + public readonly status: number; 49 + constructor(err: ErrorType) { 50 + let msg: string; 51 + let st: number; 52 + switch (err) { 53 + case ErrorType.NotFound: 54 + msg = "Book {{id}} not found"; 55 + st = 404; 56 + break; 57 + case ErrorType.InvalidId: 58 + msg = "Invalid book id ({{id}}) [must be ISBN-13 formatted]"; 59 + st = 400; 60 + break; 61 + case ErrorType.BadData: 62 + msg = "Invalid book data"; 63 + st = 400; 64 + break; 65 + case ErrorType.AlreadyExists: 66 + msg = "Book with id {{id}} already exists"; 67 + st = 409; 68 + break; 69 + } 70 + super(msg); 71 + this.name = "BookError"; 72 + this.status = st; 73 + } 74 + } 75 + 76 + const getBooks: () => Promise<Book[]> = async () => { 77 + if (!(await exists("books.json"))) { 78 + await initBooks(); 79 + } 80 + const file = await readFile("books.json", "utf-8"); 81 + if (file.length < 4) { 82 + await initBooks(); 83 + return await getBooks(); 84 + } 85 + return JSON.parse(file); 86 + }; 87 + 88 + const updateBook = async (task: Book): Promise<void> => { 89 + const books = await getBooks(); 90 + const index = books.findIndex((b) => b.id === task.id); 91 + if (index !== -1) { 92 + books[index] = task; 93 + } else { 94 + books.push(task); 95 + } 96 + await writeFile("books.json", JSON.stringify(books)); 97 + }; 98 + 99 + const removeBook = async (id: string): Promise<void> => { 100 + const books = await getBooks(); 101 + const index = books.findIndex((b) => b.id === id); 102 + if (index !== -1) { 103 + books.splice(index, 1); 104 + await writeFile("books.json", JSON.stringify(books)); 105 + } 106 + }; 107 + 108 + class BadDataIssues extends Error { 109 + missingKeys: string[]; 110 + extraKeys: string[]; 111 + badValues: [string, string][]; 112 + 113 + constructor( 114 + missingKeys: string[], 115 + extraKeys: string[], 116 + badValues: [string, string][], 117 + ) { 118 + super("Bad data issues"); 119 + this.missingKeys = missingKeys; 120 + this.extraKeys = extraKeys; 121 + this.badValues = badValues; 122 + } 123 + } 124 + 125 + const keyTypes = { 126 + id: "ISBN13 code", 127 + title: "string", 128 + author: "string", 129 + }; 130 + 131 + const validateBook = (task: { [key: string]: any }): Book => { 132 + let missingKeys = ["id", "title", "author"].filter( 133 + (key) => !Object.keys(task).includes(key), 134 + ); 135 + let extraKeys = Object.keys(task).filter( 136 + (key) => !["id", "title", "author"].includes(key), 137 + ); 138 + let badValues = Object.entries(task) 139 + .filter(([key, value]) => { 140 + if (key === "id") return typeof value !== "string" || !ISBN13.test(value); 141 + if (key === "title") return typeof value !== "string"; 142 + if (key === "author") return typeof value !== "string"; 143 + return false; 144 + }) 145 + .map( 146 + ([key, _value]) => 147 + [key, keyTypes[key as keyof typeof keyTypes]] as [string, string], 148 + ); 149 + if (missingKeys.length > 0 || extraKeys.length > 0 || badValues.length > 0) { 150 + throw new BadDataIssues(missingKeys, extraKeys, badValues); 151 + } 152 + return task as Book; 153 + }; 154 + 155 + const auth = async (req: Request, res: Response, next: NextFunction) => { 156 + if (req.method === "GET") { 157 + next(); 158 + return; 159 + } 160 + if (!req.headers.authorization) { 161 + res.status(401).json({ error: "Unauthorized" }); 162 + return; 163 + } 164 + let token = req.headers.authorization.split(" ")[1]; 165 + if (token !== "password1!") { 166 + res.status(401).json({ error: "Unauthorized" }); 167 + return; 168 + } 169 + next(); 170 + }; 171 + 172 + const errorHandler = ( 173 + err: Error, 174 + _req: Request, 175 + res: Response, 176 + _next: NextFunction, 177 + ) => { 178 + if (err instanceof BookError) { 179 + let msg = err.message.replace("{{id}}", res.locals.id ?? ""); 180 + 181 + let obj: Map<string, any> = new Map<string, any>([ 182 + ["error", `${err.name}: ${msg}`], 183 + ]); 184 + 185 + if (res.locals.bdi) { 186 + if (res.locals.bdi.missingKeys.length > 0) { 187 + obj.set("missingKeys", res.locals.bdi.missingKeys); 188 + } 189 + if (res.locals.bdi.extraKeys.length > 0) { 190 + obj.set("extraKeys", res.locals.bdi.extraKeys); 191 + } 192 + if (res.locals.bdi.badValues.length > 0) { 193 + obj.set("badValues", res.locals.bdi.badValues); 194 + } 195 + } 196 + 197 + res.status(err.status).json(Object.fromEntries(obj.entries())); 198 + } else { 199 + console.error(err.stack); 200 + res.status(500).json({ error: "Internal Server Error" }); 201 + } 202 + }; 203 + 204 + const router = express.Router(); 205 + 206 + router.use(express.json()); 207 + router.use((req, res, next) => { 208 + console.log(`Recieved a ${req.method} request to ${req.url}`); 209 + next(); 210 + }); 211 + router.use(auth); 212 + 213 + router.get("/", async (_req, res) => { 214 + res.json(await getBooks()); 215 + }); 216 + 217 + router.post("/", async (req, res) => { 218 + const books = await getBooks(); 219 + try { 220 + const bookData = validateBook(req.body); 221 + res.locals.id = bookData.id; 222 + if (books.filter((b) => b.id === bookData.id).length > 0) { 223 + throw new BookError(ErrorType.AlreadyExists); 224 + } 225 + await updateBook(bookData); 226 + res.status(201).json(bookData); 227 + } catch (err) { 228 + if (err instanceof BookError) { 229 + throw err; 230 + } else if (err instanceof BadDataIssues) { 231 + res.locals.bdi = err; 232 + throw new BookError(ErrorType.BadData); 233 + } else { 234 + res.status(500).json({ error: "Internal Server Error" }); 235 + } 236 + } 237 + }); 238 + 239 + router.get("/:id", async (req, res) => { 240 + res.locals.id = req.params.id; 241 + if (!ISBN13.test(req.params.id)) { 242 + throw new BookError(ErrorType.InvalidId); 243 + } 244 + const books = await getBooks(); 245 + const book = books.find((b) => b.id == req.params.id); 246 + if (!book) throw new BookError(ErrorType.NotFound); 247 + res.json(book); 248 + }); 249 + 250 + router.put("/:id", async (req, res) => { 251 + res.locals.id = req.params.id; 252 + if (!ISBN13.test(req.params.id)) { 253 + throw new BookError(ErrorType.InvalidId); 254 + } 255 + const books = await getBooks(); 256 + const book = books.find((b) => b.id == req.params.id); 257 + if (!book) throw new BookError(ErrorType.NotFound); 258 + const bookData = validateBook(req.body); 259 + await updateBook(bookData); 260 + res.sendStatus(204); 261 + }); 262 + 263 + router.delete("/reset", async (_req, res) => { 264 + await initBooks(); 265 + res.sendStatus(204); 266 + }); 267 + 268 + router.delete("/:id", async (req, res) => { 269 + res.locals.id = req.params.id; 270 + if (!ISBN13.test(req.params.id)) { 271 + throw new BookError(ErrorType.InvalidId); 272 + } 273 + const books = await getBooks(); 274 + const book = books.find((b) => b.id == req.params.id); 275 + if (!book) throw new BookError(ErrorType.NotFound); 276 + await removeBook(book.id); 277 + res.sendStatus(204); 278 + }); 279 + 280 + router.all("/{*splat}", async (req, res) => { 281 + res 282 + .status(404) 283 + .json({ error: `path: ${req.method} at /${req.params.splat} Not Found` }); 284 + }); 285 + 286 + router.use(errorHandler); 287 + 288 + export default router;
+177
server/bun.lock
··· 1 + { 2 + "lockfileVersion": 1, 3 + "workspaces": { 4 + "": { 5 + "name": "server", 6 + "dependencies": { 7 + "@types/express": "^5.0.6", 8 + "express": "^5.2.1", 9 + }, 10 + "devDependencies": { 11 + "@types/bun": "latest", 12 + }, 13 + "peerDependencies": { 14 + "typescript": "^5", 15 + }, 16 + }, 17 + }, 18 + "packages": { 19 + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], 20 + 21 + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], 22 + 23 + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], 24 + 25 + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], 26 + 27 + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], 28 + 29 + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], 30 + 31 + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 32 + 33 + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], 34 + 35 + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], 36 + 37 + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], 38 + 39 + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], 40 + 41 + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], 42 + 43 + "body-parser": ["body-parser@2.2.1", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw=="], 44 + 45 + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], 46 + 47 + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], 48 + 49 + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], 50 + 51 + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], 52 + 53 + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], 54 + 55 + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], 56 + 57 + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], 58 + 59 + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], 60 + 61 + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], 62 + 63 + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], 64 + 65 + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], 66 + 67 + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], 68 + 69 + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], 70 + 71 + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], 72 + 73 + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], 74 + 75 + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], 76 + 77 + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], 78 + 79 + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 80 + 81 + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], 82 + 83 + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], 84 + 85 + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], 86 + 87 + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], 88 + 89 + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], 90 + 91 + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], 92 + 93 + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], 94 + 95 + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 96 + 97 + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], 98 + 99 + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], 100 + 101 + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], 102 + 103 + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], 104 + 105 + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], 106 + 107 + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], 108 + 109 + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], 110 + 111 + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], 112 + 113 + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], 114 + 115 + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], 116 + 117 + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], 118 + 119 + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], 120 + 121 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 122 + 123 + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], 124 + 125 + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], 126 + 127 + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], 128 + 129 + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], 130 + 131 + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 132 + 133 + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], 134 + 135 + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], 136 + 137 + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], 138 + 139 + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], 140 + 141 + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], 142 + 143 + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], 144 + 145 + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], 146 + 147 + "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="], 148 + 149 + "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="], 150 + 151 + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], 152 + 153 + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], 154 + 155 + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], 156 + 157 + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], 158 + 159 + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], 160 + 161 + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 162 + 163 + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], 164 + 165 + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 166 + 167 + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 168 + 169 + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 170 + 171 + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], 172 + 173 + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], 174 + 175 + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], 176 + } 177 + }
+13
server/index.ts
··· 1 + import express from "express"; 2 + import tasks from "./tasks.ts"; 3 + import books from "./books.ts"; 4 + 5 + const app = express(); 6 + const port = process.env["NODE_PORT"] ?? 5173; 7 + 8 + app.use("/tasks", tasks); 9 + app.use("/books", books); 10 + 11 + app.listen(port, () => { 12 + console.log(`Server listening on port ${port}`); 13 + });
+16
server/package.json
··· 1 + { 2 + "name": "server", 3 + "module": "index.ts", 4 + "type": "module", 5 + "private": true, 6 + "devDependencies": { 7 + "@types/bun": "latest" 8 + }, 9 + "peerDependencies": { 10 + "typescript": "^5" 11 + }, 12 + "dependencies": { 13 + "@types/express": "^5.0.6", 14 + "express": "^5.2.1" 15 + } 16 + }
server/tasks.json

This is a binary file and will not be displayed.

+146
server/tasks.ts
··· 1 + import express from "express"; 2 + import { readFile, writeFile, exists } from "fs/promises"; 3 + 4 + const router = express.Router(); 5 + 6 + interface Task { 7 + id: string; 8 + title: string; 9 + completed: boolean; 10 + } 11 + 12 + const getTasks: () => Promise<Task[]> = async () => { 13 + if (!(await exists("tasks.json"))) { 14 + await writeFile("tasks.json", JSON.stringify([])); 15 + } 16 + const file = await readFile("tasks.json", "utf-8"); 17 + if (file.length === 0) { 18 + return []; 19 + } 20 + return JSON.parse(file); 21 + }; 22 + 23 + const updateTask = async (task: Task): Promise<void> => { 24 + const tasks = await getTasks(); 25 + const index = tasks.findIndex((t) => t.id === task.id); 26 + if (index !== -1) { 27 + tasks[index] = task; 28 + } else { 29 + tasks.push(task); 30 + } 31 + await writeFile("tasks.json", JSON.stringify(tasks)); 32 + }; 33 + 34 + const removeTask = async (id: string): Promise<void> => { 35 + const tasks = await getTasks(); 36 + const index = tasks.findIndex((t) => t.id === id); 37 + if (index !== -1) { 38 + tasks.splice(index, 1); 39 + await writeFile("tasks.json", JSON.stringify(tasks)); 40 + } 41 + }; 42 + 43 + router.use(express.json()); 44 + 45 + router.get("/", async (_req, res) => { 46 + res.json(await getTasks()); 47 + }); 48 + 49 + router.post("/", async (req, res) => { 50 + if (!(typeof req.body.title === "string")) { 51 + res.status(400).send("Invalid title"); 52 + return; 53 + } 54 + const newTask = { 55 + id: Math.random().toString(16).substring(2, 8), 56 + title: req.body.title, 57 + completed: false, 58 + }; 59 + await updateTask(newTask); 60 + res.json(newTask).status(201); 61 + }); 62 + 63 + router.get("/:id", async (req, res) => { 64 + const task = (await getTasks()).find((t) => t.id === req.params.id); 65 + if (!task) { 66 + res.status(404).send("Task not found"); 67 + } else { 68 + res.json(task); 69 + } 70 + }); 71 + 72 + router.put("/:id", async (req, res) => { 73 + const task = (await getTasks()).find((t) => t.id === req.params.id); 74 + if (!task) { 75 + res.status(404).send("Task not found"); 76 + } else { 77 + const missing = []; 78 + if (req.body.title === undefined) missing.push("title"); 79 + if (req.body.completed === undefined) missing.push("completed"); 80 + if (missing.length > 0) { 81 + res 82 + .status(400) 83 + .send( 84 + `Missing field${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}`, 85 + ); 86 + } 87 + const badTypes = []; 88 + if (!(typeof req.body.title === "string")) badTypes.push("title"); 89 + if (!(typeof req.body.completed === "boolean")) badTypes.push("completed"); 90 + if (badTypes.length > 0) { 91 + res 92 + .status(400) 93 + .send( 94 + `Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`, 95 + ); 96 + return; 97 + } 98 + task.title = req.body.title ?? task.title; 99 + task.completed = req.body.completed ?? task.completed; 100 + await updateTask(task); 101 + res.json(task); 102 + } 103 + }); 104 + 105 + router.patch("/:id", async (req, res) => { 106 + const task = (await getTasks()).find((t) => t.id === req.params.id); 107 + if (!task) { 108 + res.status(404).send("Task not found"); 109 + } else { 110 + const badTypes = []; 111 + if ( 112 + Object.keys(req.body).includes("title") && 113 + !(typeof req.body.title === "string") 114 + ) 115 + badTypes.push("title"); 116 + if ( 117 + Object.keys(req.body).includes("completed") && 118 + !(typeof req.body.completed === "boolean") 119 + ) 120 + badTypes.push("completed"); 121 + if (badTypes.length > 0) { 122 + res 123 + .status(400) 124 + .send( 125 + `Invalid type${badTypes.length > 1 ? "s" : ""}: ${badTypes.join(", ")}`, 126 + ); 127 + return; 128 + } 129 + task.title = req.body.title ?? task.title; 130 + task.completed = req.body.completed ?? task.completed; 131 + await updateTask(task); 132 + res.json(task); 133 + } 134 + }); 135 + 136 + router.delete("/:id", async (req, res) => { 137 + const task = (await getTasks()).find((t) => t.id === req.params.id); 138 + if (!task) { 139 + res.status(404).send("Task not found"); 140 + } else { 141 + await removeTask(task.id); 142 + res.status(204).send(); 143 + } 144 + }); 145 + 146 + export default router;
+29
server/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + // Environment setup & latest features 4 + "lib": ["ESNext"], 5 + "target": "ESNext", 6 + "module": "Preserve", 7 + "moduleDetection": "force", 8 + "jsx": "react-jsx", 9 + "allowJs": true, 10 + 11 + // Bundler mode 12 + "moduleResolution": "bundler", 13 + "allowImportingTsExtensions": true, 14 + "verbatimModuleSyntax": true, 15 + "noEmit": true, 16 + 17 + // Best practices 18 + "strict": true, 19 + "skipLibCheck": true, 20 + "noFallthroughCasesInSwitch": true, 21 + "noUncheckedIndexedAccess": true, 22 + "noImplicitOverride": true, 23 + 24 + // Some stricter flags (disabled by default) 25 + "noUnusedLocals": false, 26 + "noUnusedParameters": false, 27 + "noPropertyAccessFromIndexSignature": false 28 + } 29 + }