+85
html/css/form.css
+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
+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
-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
+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
+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
+15
server/README.md
+1
server/books.json
+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
+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
+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
+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
+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
server/tasks.json
This is a binary file and will not be displayed.
+146
server/tasks.ts
+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
+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
+
}