CMU Coding Bootcamp

Compare changes

Choose any two refs to compare.

Changed files
+477 -16
react
src
server
+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
+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;
+4 -13
server/index.ts
··· 1 1 import express from "express"; 2 + import tasks from "./tasks.ts"; 3 + import books from "./books.ts"; 2 4 3 5 const app = express(); 4 6 const port = process.env["NODE_PORT"] ?? 5173; 5 7 6 - app.get("/fast", (req, res) => { 7 - res.send("Fast response!"); 8 - }); 9 - 10 - app.get("/slow", (req, res) => { 11 - console.log("Slow task start"); 12 - const t = Date.now(); 13 - setTimeout(() => { 14 - let f = Date.now(); 15 - console.log(`Slow task finished in ${f - t}`); 16 - res.send(`Slow response after ${f - t} ms`); 17 - }, 5000); 18 - }); 8 + app.use("/tasks", tasks); 9 + app.use("/books", books); 19 10 20 11 app.listen(port, () => { 21 12 console.log(`Server listening on port ${port}`);
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;